@dereekb/firebase-server 13.36.0 → 13.38.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.
Files changed (45) hide show
  1. package/calcom/package.json +10 -10
  2. package/discord/package.json +10 -10
  3. package/index.cjs.js +1732 -510
  4. package/index.esm.js +1717 -513
  5. package/mailgun/package.json +9 -9
  6. package/mcp/index.cjs.js +913 -167
  7. package/mcp/index.esm.js +906 -166
  8. package/mcp/package.json +13 -12
  9. package/mcp/src/lib/controller/mcp.controller.d.ts +28 -3
  10. package/mcp/src/lib/controller/mcp.wellknown.controller.d.ts +21 -1
  11. package/mcp/src/lib/mcp.config.d.ts +31 -8
  12. package/mcp/src/lib/mcp.module.d.ts +2 -1
  13. package/mcp/src/lib/service/index.d.ts +1 -0
  14. package/mcp/src/lib/service/mcp.server.factory.d.ts +8 -6
  15. package/mcp/src/lib/service/mcp.tool-generator.d.ts +1 -1
  16. package/mcp/src/lib/service/mcp.visibility.d.ts +1 -1
  17. package/mcp/src/lib/service/tools/mcp.tool.model-roles.d.ts +95 -0
  18. package/mcp/src/lib/transport/streamable-http.transport.d.ts +16 -11
  19. package/model/index.cjs.js +1 -1
  20. package/model/index.esm.js +1 -1
  21. package/model/package.json +9 -9
  22. package/oidc/index.cjs.js +257 -67
  23. package/oidc/index.esm.js +256 -69
  24. package/oidc/package.json +10 -10
  25. package/oidc/src/lib/profile.d.ts +21 -0
  26. package/oidc/src/lib/service/index.d.ts +1 -0
  27. package/oidc/src/lib/service/oidc.config.service.d.ts +14 -0
  28. package/oidc/src/lib/service/oidc.interaction-policy.d.ts +35 -0
  29. package/oidc/src/lib/service/oidc.service.d.ts +29 -0
  30. package/package.json +14 -13
  31. package/src/lib/env/env.service.d.ts +27 -3
  32. package/src/lib/nest/controller/api.scope.d.ts +63 -0
  33. package/src/lib/nest/controller/index.d.ts +2 -0
  34. package/src/lib/nest/controller/model/model.api.get.service.d.ts +146 -1
  35. package/src/lib/nest/controller/model/model.api.scope.d.ts +3 -2
  36. package/src/lib/nest/controller/session/index.d.ts +4 -0
  37. package/src/lib/nest/controller/session/session.api.config.d.ts +104 -0
  38. package/src/lib/nest/controller/session/session.api.controller.d.ts +26 -0
  39. package/src/lib/nest/controller/session/session.api.module.d.ts +49 -0
  40. package/src/lib/nest/controller/session/session.api.service.d.ts +71 -0
  41. package/test/index.cjs.js +8 -8
  42. package/test/index.esm.js +8 -8
  43. package/test/package.json +11 -11
  44. package/twilio/package.json +8 -8
  45. package/zoho/package.json +10 -10
package/mcp/index.esm.js CHANGED
@@ -1,21 +1,20 @@
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)) {
15
14
  throw new TypeError("Cannot call a class as a function");
16
15
  }
17
16
  }
18
- function _define_property$9(obj, key, value) {
17
+ function _define_property$a(obj, key, value) {
19
18
  if (key in obj) {
20
19
  Object.defineProperty(obj, key, {
21
20
  value: value,
@@ -86,7 +85,7 @@ function _define_property$9(obj, key, value) {
86
85
  * discovery document. Claude custom-connector reads this to discover the OIDC issuer.
87
86
  *
88
87
  * @example 'https://api.example.com/oidc'
89
- */ _define_property$9(this, "oidcIssuer", void 0);
88
+ */ _define_property$a(this, "oidcIssuer", void 0);
90
89
  /**
91
90
  * The canonical resource URL of the MCP endpoint.
92
91
  *
@@ -94,17 +93,18 @@ function _define_property$9(obj, key, value) {
94
93
  * and used as the audience the access token's `aud` claim should match.
95
94
  *
96
95
  * @example 'https://api.example.com/mcp'
97
- */ _define_property$9(this, "mcpUrl", void 0);
96
+ */ _define_property$a(this, "mcpUrl", void 0);
98
97
  /**
99
98
  * Optional filter applied to the OIDC provider's scope list when building the
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
@@ -112,19 +112,19 @@ function _define_property$9(obj, key, value) {
112
112
  * the OIDC provider nothing to grant and ending the interaction in `access_denied`.
113
113
  *
114
114
  * @example (allScopes) => allScopes.filter((scope) => scope === 'openid' || scope.startsWith('model.'))
115
- */ _define_property$9(this, "scopesSupported", void 0);
115
+ */ _define_property$a(this, "scopesSupported", void 0);
116
116
  /**
117
117
  * Optional name advertised on the MCP `initialize` handshake. Defaults to
118
118
  * {@link DEFAULT_MCP_SERVER_NAME}.
119
- */ _define_property$9(this, "serverName", void 0);
119
+ */ _define_property$a(this, "serverName", void 0);
120
120
  /**
121
121
  * Optional version advertised on the MCP `initialize` handshake.
122
- */ _define_property$9(this, "serverVersion", void 0);
122
+ */ _define_property$a(this, "serverVersion", void 0);
123
123
  /**
124
124
  * Optional override for the `instructions` string passed to `new McpServer(...)` and
125
125
  * advertised on the JSON-RPC `initialize` handshake. Defaults to
126
126
  * {@link DEFAULT_MCP_SERVER_INSTRUCTIONS}.
127
- */ _define_property$9(this, "serverInstructions", void 0);
127
+ */ _define_property$a(this, "serverInstructions", void 0);
128
128
  /**
129
129
  * Absolute path to a pre-rendered MCP manifest JSON file produced by
130
130
  * `dbx-cli-generate-mcp-manifest`. When set, the runtime reads it once at
@@ -134,7 +134,7 @@ function _define_property$9(obj, key, value) {
134
134
  * Optional. When unset or the file is missing, the runtime falls back to
135
135
  * today's behavior (auto-generated descriptions, ArkType-derived input
136
136
  * schemas, no output schemas) and emits a single boot warning.
137
- */ _define_property$9(this, "mcpManifestPath", void 0);
137
+ */ _define_property$a(this, "mcpManifestPath", void 0);
138
138
  /**
139
139
  * Absolute path to a pre-rendered route manifest JSON file produced by
140
140
  * `dbx-cli-generate-route-manifest`. When set, the runtime reads it once at
@@ -144,7 +144,7 @@ function _define_property$9(obj, key, value) {
144
144
  * Optional. When unset or the file is missing, the runtime skips registering
145
145
  * `url-models` and emits a single boot warning when the path was set but
146
146
  * unreadable.
147
- */ _define_property$9(this, "mcpRouteManifestPath", void 0);
147
+ */ _define_property$a(this, "mcpRouteManifestPath", void 0);
148
148
  /**
149
149
  * When `true`, the MCP server only advertises tools whose effective read-only classification is `true`.
150
150
  *
@@ -154,7 +154,7 @@ function _define_property$9(obj, key, value) {
154
154
  *
155
155
  * The advertised `serverName` on the JSON-RPC `initialize` handshake is suffixed with
156
156
  * ` (read-only)` so the client surface reflects the mode.
157
- */ _define_property$9(this, "readOnly", void 0);
157
+ */ _define_property$a(this, "readOnly", void 0);
158
158
  /**
159
159
  * Controls the auto-injected `reason` parameter added to every advertised tool's input schema.
160
160
  *
@@ -164,7 +164,7 @@ function _define_property$9(obj, key, value) {
164
164
  * When enabled, every advertised tool's `inputSchema` carries a required `reason` string the model
165
165
  * fills with a short justification for the call. The value is forwarded to analytics and stripped
166
166
  * from the dispatched handler body. See {@link McpReasonParameterConfig}.
167
- */ _define_property$9(this, "reasonParameter", void 0);
167
+ */ _define_property$a(this, "reasonParameter", void 0);
168
168
  /**
169
169
  * Default OIDC scope group term required on EVERY `callModel` op, mirrored into MCP tool-list
170
170
  * visibility, unless a finer term overrides it (a per-function `requiredScope`, then a
@@ -174,17 +174,20 @@ function _define_property$9(obj, key, value) {
174
174
  *
175
175
  * Set this to the SAME value passed to `ModelApiDispatchConfig.defaultRequiredScope` to
176
176
  * keep tool visibility and enforcement in lockstep.
177
- */ _define_property$9(this, "defaultRequiredScope", void 0);
177
+ */ _define_property$a(this, "defaultRequiredScope", void 0);
178
178
  /**
179
179
  * Per-model OIDC scope group-term overrides, keyed by {@link FirestoreModelType}, mirrored into MCP
180
180
  * tool-list visibility. Set this to the SAME value passed to
181
181
  * `ModelApiDispatchConfig.modelRequiredScopes` so per-model / verb-keyed restrictions that
182
182
  * confine a subset client apply identically to the advertised tool list.
183
- */ _define_property$9(this, "modelRequiredScopes", void 0);
183
+ */ _define_property$a(this, "modelRequiredScopes", void 0);
184
184
  };
185
185
  /**
186
186
  * NestJS injection token for the optional {@link McpAuthRoleReader} provider.
187
187
  */ var MCP_AUTH_ROLE_READER = 'MCP_AUTH_ROLE_READER';
188
+ /**
189
+ * NestJS injection token for the optional {@link McpModelRolesTargetUidPredicate} provider.
190
+ */ var MCP_MODEL_ROLES_TARGET_UID_PREDICATE = 'MCP_MODEL_ROLES_TARGET_UID_PREDICATE';
188
191
 
189
192
  function _type_of$5(obj) {
190
193
  "@swc/helpers - typeof";
@@ -214,7 +217,7 @@ function _array_like_to_array$b(arr, len) {
214
217
  function _array_without_holes$8(arr) {
215
218
  if (Array.isArray(arr)) return _array_like_to_array$b(arr);
216
219
  }
217
- function _define_property$8(obj, key, value) {
220
+ function _define_property$9(obj, key, value) {
218
221
  if (key in obj) {
219
222
  Object.defineProperty(obj, key, {
220
223
  value: value,
@@ -233,7 +236,7 @@ function _iterable_to_array$8(iter) {
233
236
  function _non_iterable_spread$8() {
234
237
  throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
235
238
  }
236
- function _object_spread$6(target) {
239
+ function _object_spread$7(target) {
237
240
  for(var i = 1; i < arguments.length; i++){
238
241
  var source = arguments[i] != null ? arguments[i] : {};
239
242
  var ownKeys = Object.keys(source);
@@ -243,7 +246,7 @@ function _object_spread$6(target) {
243
246
  }));
244
247
  }
245
248
  ownKeys.forEach(function(key) {
246
- _define_property$8(target, key, source[key]);
249
+ _define_property$9(target, key, source[key]);
247
250
  });
248
251
  }
249
252
  return target;
@@ -353,9 +356,9 @@ function _unsupported_iterable_to_array$b(o, minLen) {
353
356
  var _base_type;
354
357
  var base = (typeof inputSchema === "undefined" ? "undefined" : _type_of$4(inputSchema)) === 'object' && inputSchema != null ? inputSchema : {};
355
358
  var existingProperties = _type_of$4(base.properties) === 'object' && base.properties != null ? base.properties : {};
356
- var next = _object_spread_props$3(_object_spread$6({}, base), {
359
+ var next = _object_spread_props$3(_object_spread$7({}, base), {
357
360
  type: (_base_type = base.type) !== null && _base_type !== void 0 ? _base_type : 'object',
358
- properties: _object_spread_props$3(_object_spread$6({}, existingProperties), _define_property$8({}, resolved.parameterName, {
361
+ properties: _object_spread_props$3(_object_spread$7({}, existingProperties), _define_property$9({}, resolved.parameterName, {
359
362
  type: 'string',
360
363
  maxLength: resolved.maxLength,
361
364
  description: resolved.description
@@ -389,7 +392,7 @@ function _unsupported_iterable_to_array$b(o, minLen) {
389
392
  var result;
390
393
  if (resolved.enabled && !toolDeclaresOwnReason && Object.prototype.hasOwnProperty.call(args, resolved.parameterName)) {
391
394
  var rawValue = args[resolved.parameterName];
392
- var nextArgs = _object_spread$6({}, args);
395
+ var nextArgs = _object_spread$7({}, args);
393
396
  delete nextArgs[resolved.parameterName];
394
397
  var stringValue = rawValue == null ? '' : String(rawValue);
395
398
  var clamped = stringValue.length > resolved.maxLength ? stringValue.slice(0, resolved.maxLength) : stringValue;
@@ -744,7 +747,7 @@ function scoreCandidates(states, pathname) {
744
747
  });
745
748
  }
746
749
 
747
- function asyncGeneratorStep$6(gen, resolve, reject, _next, _throw, key, arg) {
750
+ function asyncGeneratorStep$7(gen, resolve, reject, _next, _throw, key, arg) {
748
751
  try {
749
752
  var info = gen[key](arg);
750
753
  var value = info.value;
@@ -758,16 +761,16 @@ function asyncGeneratorStep$6(gen, resolve, reject, _next, _throw, key, arg) {
758
761
  Promise.resolve(value).then(_next, _throw);
759
762
  }
760
763
  }
761
- function _async_to_generator$6(fn) {
764
+ function _async_to_generator$7(fn) {
762
765
  return function() {
763
766
  var self = this, args = arguments;
764
767
  return new Promise(function(resolve, reject) {
765
768
  var gen = fn.apply(self, args);
766
769
  function _next(value) {
767
- asyncGeneratorStep$6(gen, resolve, reject, _next, _throw, "next", value);
770
+ asyncGeneratorStep$7(gen, resolve, reject, _next, _throw, "next", value);
768
771
  }
769
772
  function _throw(err) {
770
- asyncGeneratorStep$6(gen, resolve, reject, _next, _throw, "throw", err);
773
+ asyncGeneratorStep$7(gen, resolve, reject, _next, _throw, "throw", err);
771
774
  }
772
775
  _next(undefined);
773
776
  });
@@ -781,7 +784,7 @@ function _instanceof$1(left, right) {
781
784
  return left instanceof right;
782
785
  }
783
786
  }
784
- function _ts_generator$6(thisArg, body) {
787
+ function _ts_generator$7(thisArg, body) {
785
788
  var f, y, t, _ = {
786
789
  label: 0,
787
790
  sent: function() {
@@ -909,9 +912,9 @@ function _ts_generator$6(thisArg, body) {
909
912
  * @param details - The handler-level API details (carries the mapper + Tier 2/3 formatters).
910
913
  * @returns The MCP tool response content.
911
914
  */ function formatMcpToolResponse(result, params, details) {
912
- return _async_to_generator$6(function() {
915
+ return _async_to_generator$7(function() {
913
916
  var mcp, value, _tmp, context, response, summary;
914
- return _ts_generator$6(this, function(_state) {
917
+ return _ts_generator$7(this, function(_state) {
915
918
  switch(_state.label){
916
919
  case 0:
917
920
  mcp = details === null || details === void 0 ? void 0 : details.mcp;
@@ -1771,7 +1774,7 @@ function _array_like_to_array$8(arr, len) {
1771
1774
  function _array_without_holes$7(arr) {
1772
1775
  if (Array.isArray(arr)) return _array_like_to_array$8(arr);
1773
1776
  }
1774
- function asyncGeneratorStep$5(gen, resolve, reject, _next, _throw, key, arg) {
1777
+ function asyncGeneratorStep$6(gen, resolve, reject, _next, _throw, key, arg) {
1775
1778
  try {
1776
1779
  var info = gen[key](arg);
1777
1780
  var value = info.value;
@@ -1785,16 +1788,16 @@ function asyncGeneratorStep$5(gen, resolve, reject, _next, _throw, key, arg) {
1785
1788
  Promise.resolve(value).then(_next, _throw);
1786
1789
  }
1787
1790
  }
1788
- function _async_to_generator$5(fn) {
1791
+ function _async_to_generator$6(fn) {
1789
1792
  return function() {
1790
1793
  var self = this, args = arguments;
1791
1794
  return new Promise(function(resolve, reject) {
1792
1795
  var gen = fn.apply(self, args);
1793
1796
  function _next(value) {
1794
- asyncGeneratorStep$5(gen, resolve, reject, _next, _throw, "next", value);
1797
+ asyncGeneratorStep$6(gen, resolve, reject, _next, _throw, "next", value);
1795
1798
  }
1796
1799
  function _throw(err) {
1797
- asyncGeneratorStep$5(gen, resolve, reject, _next, _throw, "throw", err);
1800
+ asyncGeneratorStep$6(gen, resolve, reject, _next, _throw, "throw", err);
1798
1801
  }
1799
1802
  _next(undefined);
1800
1803
  });
@@ -1817,7 +1820,7 @@ function _unsupported_iterable_to_array$8(o, minLen) {
1817
1820
  if (n === "Map" || n === "Set") return Array.from(n);
1818
1821
  if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array$8(o, minLen);
1819
1822
  }
1820
- function _ts_generator$5(thisArg, body) {
1823
+ function _ts_generator$6(thisArg, body) {
1821
1824
  var f, y, t, _ = {
1822
1825
  label: 0,
1823
1826
  sent: function() {
@@ -1978,55 +1981,564 @@ function _ts_generator$5(thisArg, body) {
1978
1981
  }
1979
1982
  // MARK: Handler
1980
1983
  function modelGetToolHandler(args, ctx, deps) {
1984
+ return _async_to_generator$6(function() {
1985
+ var result, input, identity, resolvedKeys, merged, error;
1986
+ return _ts_generator$6(this, function(_state) {
1987
+ switch(_state.label){
1988
+ case 0:
1989
+ _state.trys.push([
1990
+ 0,
1991
+ 2,
1992
+ ,
1993
+ 3
1994
+ ]);
1995
+ input = parseModelGetInput(args);
1996
+ identity = deps.resolveIdentity(input.modelType, ctx.auth);
1997
+ if (identity == null) {
1998
+ throw new Error("Unknown modelType: ".concat(input.modelType));
1999
+ }
2000
+ resolvedKeys = resolveKeys$1(input.keys, identity);
2001
+ return [
2002
+ 4,
2003
+ readInChunks$1({
2004
+ modelType: input.modelType,
2005
+ keys: resolvedKeys,
2006
+ auth: ctx.auth,
2007
+ readDocuments: deps.readDocuments
2008
+ })
2009
+ ];
2010
+ case 1:
2011
+ merged = _state.sent();
2012
+ result = {
2013
+ content: [
2014
+ {
2015
+ type: 'text',
2016
+ text: JSON.stringify(merged)
2017
+ }
2018
+ ],
2019
+ structuredContent: merged
2020
+ };
2021
+ return [
2022
+ 3,
2023
+ 3
2024
+ ];
2025
+ case 2:
2026
+ error = _state.sent();
2027
+ result = formatMcpToolErrorResponse(error);
2028
+ return [
2029
+ 3,
2030
+ 3
2031
+ ];
2032
+ case 3:
2033
+ return [
2034
+ 2,
2035
+ result
2036
+ ];
2037
+ }
2038
+ });
2039
+ })();
2040
+ }
2041
+ function parseModelGetInput(args) {
2042
+ var modelType = args['modelType'];
2043
+ var keys = args['keys'];
2044
+ if (typeof modelType !== 'string' || modelType.length === 0) {
2045
+ throw new Error('model-get: "modelType" is required and must be a non-empty string.');
2046
+ }
2047
+ if (!Array.isArray(keys) || keys.length === 0) {
2048
+ throw new Error('model-get: "keys" is required and must be a non-empty array.');
2049
+ }
2050
+ var normalizedKeys = keys.map(function(value, index) {
2051
+ if (typeof value !== 'string' || value.length === 0) {
2052
+ throw new Error("model-get: keys[".concat(index, "] must be a non-empty string."));
2053
+ }
2054
+ return value;
2055
+ });
2056
+ return {
2057
+ modelType: modelType,
2058
+ keys: normalizedKeys
2059
+ };
2060
+ }
2061
+ function resolveKeys$1(keys, identity) {
2062
+ var isRoot = identity.type === 'root';
2063
+ var resolved = [];
2064
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
2065
+ try {
2066
+ for(var _iterator = keys[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
2067
+ var value = _step.value;
2068
+ if (value.includes('/')) {
2069
+ resolved.push(value);
2070
+ } else if (isRoot) {
2071
+ resolved.push("".concat(identity.collectionName, "/").concat(value));
2072
+ } else {
2073
+ throw new Error('model-get: modelType "'.concat(identity.modelType, '" is a subcollection; bare ids are not allowed. Provide full keys (e.g. "parentPrefix/parentId/').concat(identity.collectionName, "/").concat(value, '").'));
2074
+ }
2075
+ }
2076
+ } catch (err) {
2077
+ _didIteratorError = true;
2078
+ _iteratorError = err;
2079
+ } finally{
2080
+ try {
2081
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
2082
+ _iterator.return();
2083
+ }
2084
+ } finally{
2085
+ if (_didIteratorError) {
2086
+ throw _iteratorError;
2087
+ }
2088
+ }
2089
+ }
2090
+ return resolved;
2091
+ }
2092
+ function readInChunks$1(input) {
2093
+ return _async_to_generator$6(function() {
2094
+ var modelType, keys, auth, readDocuments, merged, offset, _merged_results, _merged_errors, batch, partial;
2095
+ return _ts_generator$6(this, function(_state) {
2096
+ switch(_state.label){
2097
+ case 0:
2098
+ modelType = input.modelType, keys = input.keys, auth = input.auth, readDocuments = input.readDocuments;
2099
+ merged = {
2100
+ results: [],
2101
+ errors: []
2102
+ };
2103
+ offset = 0;
2104
+ _state.label = 1;
2105
+ case 1:
2106
+ if (!(offset < keys.length)) return [
2107
+ 3,
2108
+ 4
2109
+ ];
2110
+ batch = keys.slice(offset, offset + MCP_MODEL_GET_BATCH_SIZE);
2111
+ return [
2112
+ 4,
2113
+ readDocuments(modelType, batch, auth)
2114
+ ];
2115
+ case 2:
2116
+ partial = _state.sent();
2117
+ (_merged_results = merged.results).push.apply(_merged_results, _to_consumable_array$7(partial.results));
2118
+ (_merged_errors = merged.errors).push.apply(_merged_errors, _to_consumable_array$7(partial.errors));
2119
+ _state.label = 3;
2120
+ case 3:
2121
+ offset += MCP_MODEL_GET_BATCH_SIZE;
2122
+ return [
2123
+ 3,
2124
+ 1
2125
+ ];
2126
+ case 4:
2127
+ return [
2128
+ 2,
2129
+ merged
2130
+ ];
2131
+ }
2132
+ });
2133
+ })();
2134
+ }
2135
+ // MARK: Schemas
2136
+ var MODEL_GET_INPUT_SCHEMA = {
2137
+ type: 'object',
2138
+ required: [
2139
+ 'modelType',
2140
+ 'keys'
2141
+ ],
2142
+ properties: {
2143
+ modelType: {
2144
+ type: 'string',
2145
+ minLength: 1,
2146
+ description: 'Firestore model type to fetch (e.g. "guestbook", "profile").'
2147
+ },
2148
+ keys: {
2149
+ type: 'array',
2150
+ minItems: 1,
2151
+ description: 'Full keys ("prefix/id") or bare ids (root models only).',
2152
+ items: {
2153
+ type: 'string',
2154
+ minLength: 1
2155
+ }
2156
+ }
2157
+ },
2158
+ additionalProperties: false
2159
+ };
2160
+ var MODEL_GET_OUTPUT_SCHEMA = {
2161
+ type: 'object',
2162
+ required: [
2163
+ 'results',
2164
+ 'errors'
2165
+ ],
2166
+ properties: {
2167
+ results: {
2168
+ type: 'array',
2169
+ description: 'Per-key successful reads.',
2170
+ items: {
2171
+ type: 'object',
2172
+ required: [
2173
+ 'key',
2174
+ 'data'
2175
+ ],
2176
+ properties: {
2177
+ key: {
2178
+ type: 'string'
2179
+ },
2180
+ data: {}
2181
+ }
2182
+ }
2183
+ },
2184
+ errors: {
2185
+ type: 'array',
2186
+ description: 'Per-key failures (not-found, forbidden, etc.).',
2187
+ items: {
2188
+ type: 'object',
2189
+ required: [
2190
+ 'key',
2191
+ 'message'
2192
+ ],
2193
+ properties: {
2194
+ key: {
2195
+ type: 'string'
2196
+ },
2197
+ message: {
2198
+ type: 'string'
2199
+ },
2200
+ code: {
2201
+ type: 'string'
2202
+ }
2203
+ }
2204
+ }
2205
+ }
2206
+ }
2207
+ };
2208
+
2209
+ function asyncGeneratorStep$5(gen, resolve, reject, _next, _throw, key, arg) {
2210
+ try {
2211
+ var info = gen[key](arg);
2212
+ var value = info.value;
2213
+ } catch (error) {
2214
+ reject(error);
2215
+ return;
2216
+ }
2217
+ if (info.done) {
2218
+ resolve(value);
2219
+ } else {
2220
+ Promise.resolve(value).then(_next, _throw);
2221
+ }
2222
+ }
2223
+ function _async_to_generator$5(fn) {
2224
+ return function() {
2225
+ var self = this, args = arguments;
2226
+ return new Promise(function(resolve, reject) {
2227
+ var gen = fn.apply(self, args);
2228
+ function _next(value) {
2229
+ asyncGeneratorStep$5(gen, resolve, reject, _next, _throw, "next", value);
2230
+ }
2231
+ function _throw(err) {
2232
+ asyncGeneratorStep$5(gen, resolve, reject, _next, _throw, "throw", err);
2233
+ }
2234
+ _next(undefined);
2235
+ });
2236
+ };
2237
+ }
2238
+ function _define_property$8(obj, key, value) {
2239
+ if (key in obj) {
2240
+ Object.defineProperty(obj, key, {
2241
+ value: value,
2242
+ enumerable: true,
2243
+ configurable: true,
2244
+ writable: true
2245
+ });
2246
+ } else {
2247
+ obj[key] = value;
2248
+ }
2249
+ return obj;
2250
+ }
2251
+ function _object_spread$6(target) {
2252
+ for(var i = 1; i < arguments.length; i++){
2253
+ var source = arguments[i] != null ? arguments[i] : {};
2254
+ var ownKeys = Object.keys(source);
2255
+ if (typeof Object.getOwnPropertySymbols === "function") {
2256
+ ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
2257
+ return Object.getOwnPropertyDescriptor(source, sym).enumerable;
2258
+ }));
2259
+ }
2260
+ ownKeys.forEach(function(key) {
2261
+ _define_property$8(target, key, source[key]);
2262
+ });
2263
+ }
2264
+ return target;
2265
+ }
2266
+ function _ts_generator$5(thisArg, body) {
2267
+ var f, y, t, _ = {
2268
+ label: 0,
2269
+ sent: function() {
2270
+ if (t[0] & 1) throw t[1];
2271
+ return t[1];
2272
+ },
2273
+ trys: [],
2274
+ ops: []
2275
+ }, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype), d = Object.defineProperty;
2276
+ return d(g, "next", {
2277
+ value: verb(0)
2278
+ }), d(g, "throw", {
2279
+ value: verb(1)
2280
+ }), d(g, "return", {
2281
+ value: verb(2)
2282
+ }), typeof Symbol === "function" && d(g, Symbol.iterator, {
2283
+ value: function() {
2284
+ return this;
2285
+ }
2286
+ }), g;
2287
+ function verb(n) {
2288
+ return function(v) {
2289
+ return step([
2290
+ n,
2291
+ v
2292
+ ]);
2293
+ };
2294
+ }
2295
+ function step(op) {
2296
+ if (f) throw new TypeError("Generator is already executing.");
2297
+ while(g && (g = 0, op[0] && (_ = 0)), _)try {
2298
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
2299
+ if (y = 0, t) op = [
2300
+ op[0] & 2,
2301
+ t.value
2302
+ ];
2303
+ switch(op[0]){
2304
+ case 0:
2305
+ case 1:
2306
+ t = op;
2307
+ break;
2308
+ case 4:
2309
+ _.label++;
2310
+ return {
2311
+ value: op[1],
2312
+ done: false
2313
+ };
2314
+ case 5:
2315
+ _.label++;
2316
+ y = op[1];
2317
+ op = [
2318
+ 0
2319
+ ];
2320
+ continue;
2321
+ case 7:
2322
+ op = _.ops.pop();
2323
+ _.trys.pop();
2324
+ continue;
2325
+ default:
2326
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
2327
+ _ = 0;
2328
+ continue;
2329
+ }
2330
+ if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
2331
+ _.label = op[1];
2332
+ break;
2333
+ }
2334
+ if (op[0] === 6 && _.label < t[1]) {
2335
+ _.label = t[1];
2336
+ t = op;
2337
+ break;
2338
+ }
2339
+ if (t && _.label < t[2]) {
2340
+ _.label = t[2];
2341
+ _.ops.push(op);
2342
+ break;
2343
+ }
2344
+ if (t[2]) _.ops.pop();
2345
+ _.trys.pop();
2346
+ continue;
2347
+ }
2348
+ op = body.call(thisArg, _);
2349
+ } catch (e) {
2350
+ op = [
2351
+ 6,
2352
+ e
2353
+ ];
2354
+ y = 0;
2355
+ } finally{
2356
+ f = t = 0;
2357
+ }
2358
+ if (op[0] & 5) throw op[1];
2359
+ return {
2360
+ value: op[0] ? op[1] : void 0,
2361
+ done: true
2362
+ };
2363
+ }
2364
+ }
2365
+ // MARK: Constants
2366
+ /**
2367
+ * Reserved tool name for the built-in `model-roles` static tool.
2368
+ */ var MODEL_ROLES_TOOL_NAME = 'model-roles';
2369
+ /**
2370
+ * Synthetic call type used in the tool's dispatch identity. Distinct from `model-get`'s `get` so
2371
+ * visibility predicates can target the two independently.
2372
+ */ var MODEL_ROLES_DISPATCH_CALL = 'roles';
2373
+ /**
2374
+ * Synthetic model type used in the tool's dispatch identity. Mirrors {@link MODEL_GET_DISPATCH_MODEL_TYPE}
2375
+ * — the tool isn't bound to one model type, so the literal "model" stands in.
2376
+ */ var MODEL_ROLES_DISPATCH_MODEL_TYPE = 'model';
2377
+ /**
2378
+ * Maximum number of keys accepted per `model-roles` call. Role resolution runs the model's real
2379
+ * permission delegate per key (which may itself read parent documents), so this is deliberately
2380
+ * lower than the `model-get` batch size.
2381
+ */ var MCP_MODEL_ROLES_MAX_KEYS = 25;
2382
+ // MARK: Factory
2383
+ /**
2384
+ * Builds the built-in `model-roles` MCP tool definition.
2385
+ *
2386
+ * Answers "what is this user actually allowed to do with this document?" by running the same
2387
+ * `roleMapForModel()` delegate the permission-checked read/write paths use, and returning the
2388
+ * resolved roles instead of consuming them. Because the model's real delegate runs, derived and
2389
+ * cascading roles appear exactly as the API grants them — there is no second implementation of the
2390
+ * rules to drift.
2391
+ *
2392
+ * Two things it disambiguates that a plain read cannot:
2393
+ * - **Missing vs. forbidden.** A key that resolves to a missing document returns
2394
+ * `exists: false, roles: []` rather than an error, so "the document is not there" reads
2395
+ * differently from "it is there and you have no access" (`exists: true, roles: []`).
2396
+ * - **Full access.** Admin short-circuits that grant the full-access marker come back as
2397
+ * `fullAccess: true` rather than as an opaque enumerated set.
2398
+ *
2399
+ * By default roles are resolved for the calling user. Passing `uid` resolves them for another user
2400
+ * and is gated behind {@link CreateModelRolesToolDeps.canTargetOtherUids}.
2401
+ *
2402
+ * @param deps - Role-map reader, identity resolver, and the target-uid authorization predicate.
2403
+ * @returns A statically-registered {@link McpToolDefinition} ready to be appended to the MCP
2404
+ * server factory's tool registry.
2405
+ */ function createModelRolesTool(deps) {
2406
+ var handler = function handler(args, ctx) {
2407
+ return modelRolesToolHandler(args, ctx, deps);
2408
+ };
2409
+ var name = MODEL_ROLES_TOOL_NAME;
2410
+ var description = 'Resolve which roles the current user (or, with "uid", another user) is granted on one or more model keys — the same permission computation the API runs, returned instead of enforced. Use to answer "why was this forbidden?" and to tell a missing document (exists:false) apart from one you cannot access (exists:true, roles:[]). Admin full-access short-circuits report fullAccess:true. Targeting another uid requires elevated access.';
2411
+ return buildStaticToolDefinition({
2412
+ name: name,
2413
+ description: description,
2414
+ inputSchema: MODEL_ROLES_INPUT_SCHEMA,
2415
+ outputSchema: MODEL_ROLES_OUTPUT_SCHEMA,
2416
+ dispatch: {
2417
+ call: MODEL_ROLES_DISPATCH_CALL,
2418
+ modelType: MODEL_ROLES_DISPATCH_MODEL_TYPE
2419
+ },
2420
+ staticHandler: handler,
2421
+ effectiveReadOnly: true,
2422
+ rule: {
2423
+ requireAuthenticated: true
2424
+ }
2425
+ });
2426
+ }
2427
+ // MARK: Handler
2428
+ function modelRolesToolHandler(args, ctx, deps) {
1981
2429
  return _async_to_generator$5(function() {
1982
- var result, input, identity, resolvedKeys, merged, error;
2430
+ var result, input, identity, targetUid, keys, resolved, error;
1983
2431
  return _ts_generator$5(this, function(_state) {
1984
2432
  switch(_state.label){
1985
2433
  case 0:
1986
2434
  _state.trys.push([
1987
2435
  0,
1988
- 2,
2436
+ 3,
1989
2437
  ,
1990
- 3
2438
+ 4
1991
2439
  ]);
1992
- input = parseModelGetInput(args);
2440
+ input = parseModelRolesInput(args);
1993
2441
  identity = deps.resolveIdentity(input.modelType, ctx.auth);
1994
2442
  if (identity == null) {
1995
2443
  throw new Error("Unknown modelType: ".concat(input.modelType));
1996
2444
  }
1997
- resolvedKeys = resolveKeys(input.keys, identity);
1998
2445
  return [
1999
2446
  4,
2000
- readInChunks$1({
2001
- modelType: input.modelType,
2002
- keys: resolvedKeys,
2447
+ resolveTargetUid({
2448
+ requestedUid: input.uid,
2003
2449
  auth: ctx.auth,
2004
- readDocuments: deps.readDocuments
2450
+ canTargetOtherUids: deps.canTargetOtherUids
2005
2451
  })
2006
2452
  ];
2007
2453
  case 1:
2008
- merged = _state.sent();
2454
+ targetUid = _state.sent();
2455
+ keys = resolveKeys(input.keys, identity);
2456
+ return [
2457
+ 4,
2458
+ deps.readRoleMaps(_object_spread$6({
2459
+ modelType: input.modelType,
2460
+ keys: keys,
2461
+ auth: ctx.auth
2462
+ }, targetUid == null ? {} : {
2463
+ targetUid: targetUid
2464
+ }))
2465
+ ];
2466
+ case 2:
2467
+ resolved = _state.sent();
2009
2468
  result = {
2010
2469
  content: [
2011
2470
  {
2012
2471
  type: 'text',
2013
- text: JSON.stringify(merged)
2472
+ text: JSON.stringify(resolved)
2014
2473
  }
2015
2474
  ],
2016
- structuredContent: merged
2475
+ structuredContent: resolved
2017
2476
  };
2018
2477
  return [
2019
2478
  3,
2020
- 3
2479
+ 4
2021
2480
  ];
2022
- case 2:
2481
+ case 3:
2023
2482
  error = _state.sent();
2024
2483
  result = formatMcpToolErrorResponse(error);
2484
+ return [
2485
+ 3,
2486
+ 4
2487
+ ];
2488
+ case 4:
2489
+ return [
2490
+ 2,
2491
+ result
2492
+ ];
2493
+ }
2494
+ });
2495
+ })();
2496
+ }
2497
+ /**
2498
+ * Resolves the effective target uid for a call, enforcing the elevated-access gate.
2499
+ *
2500
+ * Requesting your own uid is always allowed and is treated as no target at all, so a caller that
2501
+ * passes their own uid never trips the predicate.
2502
+ *
2503
+ * @param input - The requested uid, the caller's auth, and the authorization predicate.
2504
+ * @returns The uid to resolve as, or `undefined` to resolve as the caller.
2505
+ * @throws {Error} When another user's uid is requested and the predicate denies (or is absent).
2506
+ */ function resolveTargetUid(input) {
2507
+ return _async_to_generator$5(function() {
2508
+ var requestedUid, auth, canTargetOtherUids, result, allowed, _tmp;
2509
+ return _ts_generator$5(this, function(_state) {
2510
+ switch(_state.label){
2511
+ case 0:
2512
+ requestedUid = input.requestedUid, auth = input.auth, canTargetOtherUids = input.canTargetOtherUids;
2513
+ if (!(requestedUid != null && requestedUid !== (auth === null || auth === void 0 ? void 0 : auth.uid))) return [
2514
+ 3,
2515
+ 4
2516
+ ];
2517
+ if (!(canTargetOtherUids == null)) return [
2518
+ 3,
2519
+ 1
2520
+ ];
2521
+ _tmp = false;
2025
2522
  return [
2026
2523
  3,
2027
2524
  3
2028
2525
  ];
2526
+ case 1:
2527
+ return [
2528
+ 4,
2529
+ canTargetOtherUids(auth)
2530
+ ];
2531
+ case 2:
2532
+ _tmp = _state.sent();
2533
+ _state.label = 3;
2029
2534
  case 3:
2535
+ allowed = _tmp;
2536
+ if (!allowed) {
2537
+ throw new Error("model-roles: resolving roles for another user's uid requires elevated access.");
2538
+ }
2539
+ result = requestedUid;
2540
+ _state.label = 4;
2541
+ case 4:
2030
2542
  return [
2031
2543
  2,
2032
2544
  result
@@ -2035,25 +2547,34 @@ function modelGetToolHandler(args, ctx, deps) {
2035
2547
  });
2036
2548
  })();
2037
2549
  }
2038
- function parseModelGetInput(args) {
2039
- var modelType = args.modelType;
2040
- var keys = args.keys;
2550
+ function parseModelRolesInput(args) {
2551
+ var modelType = args['modelType'];
2552
+ var keys = args['keys'];
2553
+ var uid = args['uid'];
2041
2554
  if (typeof modelType !== 'string' || modelType.length === 0) {
2042
- throw new Error('model-get: "modelType" is required and must be a non-empty string.');
2555
+ throw new Error('model-roles: "modelType" is required and must be a non-empty string.');
2043
2556
  }
2044
2557
  if (!Array.isArray(keys) || keys.length === 0) {
2045
- throw new Error('model-get: "keys" is required and must be a non-empty array.');
2558
+ throw new Error('model-roles: "keys" is required and must be a non-empty array.');
2559
+ }
2560
+ if (keys.length > MCP_MODEL_ROLES_MAX_KEYS) {
2561
+ throw new Error("model-roles: at most ".concat(MCP_MODEL_ROLES_MAX_KEYS, " keys may be resolved per call (received ").concat(keys.length, ")."));
2562
+ }
2563
+ if (uid !== undefined && (typeof uid !== 'string' || uid.length === 0)) {
2564
+ throw new Error('model-roles: "uid" must be a non-empty string when provided.');
2046
2565
  }
2047
2566
  var normalizedKeys = keys.map(function(value, index) {
2048
2567
  if (typeof value !== 'string' || value.length === 0) {
2049
- throw new Error("model-get: keys[".concat(index, "] must be a non-empty string."));
2568
+ throw new Error("model-roles: keys[".concat(index, "] must be a non-empty string."));
2050
2569
  }
2051
2570
  return value;
2052
2571
  });
2053
- return {
2572
+ return _object_spread$6({
2054
2573
  modelType: modelType,
2055
2574
  keys: normalizedKeys
2056
- };
2575
+ }, uid === undefined ? {} : {
2576
+ uid: uid
2577
+ });
2057
2578
  }
2058
2579
  function resolveKeys(keys, identity) {
2059
2580
  var isRoot = identity.type === 'root';
@@ -2067,7 +2588,7 @@ function resolveKeys(keys, identity) {
2067
2588
  } else if (isRoot) {
2068
2589
  resolved.push("".concat(identity.collectionName, "/").concat(value));
2069
2590
  } else {
2070
- throw new Error('model-get: modelType "'.concat(identity.modelType, '" is a subcollection; bare ids are not allowed. Provide full keys (e.g. "parentPrefix/parentId/').concat(identity.collectionName, "/").concat(value, '").'));
2591
+ throw new Error('model-roles: modelType "'.concat(identity.modelType, '" is a subcollection; bare ids are not allowed. Provide full keys (e.g. "parentPrefix/parentId/').concat(identity.collectionName, "/").concat(value, '").'));
2071
2592
  }
2072
2593
  }
2073
2594
  } catch (err) {
@@ -2086,51 +2607,8 @@ function resolveKeys(keys, identity) {
2086
2607
  }
2087
2608
  return resolved;
2088
2609
  }
2089
- function readInChunks$1(input) {
2090
- return _async_to_generator$5(function() {
2091
- var modelType, keys, auth, readDocuments, merged, offset, _merged_results, _merged_errors, batch, partial;
2092
- return _ts_generator$5(this, function(_state) {
2093
- switch(_state.label){
2094
- case 0:
2095
- modelType = input.modelType, keys = input.keys, auth = input.auth, readDocuments = input.readDocuments;
2096
- merged = {
2097
- results: [],
2098
- errors: []
2099
- };
2100
- offset = 0;
2101
- _state.label = 1;
2102
- case 1:
2103
- if (!(offset < keys.length)) return [
2104
- 3,
2105
- 4
2106
- ];
2107
- batch = keys.slice(offset, offset + MCP_MODEL_GET_BATCH_SIZE);
2108
- return [
2109
- 4,
2110
- readDocuments(modelType, batch, auth)
2111
- ];
2112
- case 2:
2113
- partial = _state.sent();
2114
- (_merged_results = merged.results).push.apply(_merged_results, _to_consumable_array$7(partial.results));
2115
- (_merged_errors = merged.errors).push.apply(_merged_errors, _to_consumable_array$7(partial.errors));
2116
- _state.label = 3;
2117
- case 3:
2118
- offset += MCP_MODEL_GET_BATCH_SIZE;
2119
- return [
2120
- 3,
2121
- 1
2122
- ];
2123
- case 4:
2124
- return [
2125
- 2,
2126
- merged
2127
- ];
2128
- }
2129
- });
2130
- })();
2131
- }
2132
2610
  // MARK: Schemas
2133
- var MODEL_GET_INPUT_SCHEMA = {
2611
+ var MODEL_ROLES_INPUT_SCHEMA = {
2134
2612
  type: 'object',
2135
2613
  required: [
2136
2614
  'modelType',
@@ -2140,47 +2618,78 @@ var MODEL_GET_INPUT_SCHEMA = {
2140
2618
  modelType: {
2141
2619
  type: 'string',
2142
2620
  minLength: 1,
2143
- description: 'Firestore model type to fetch (e.g. "guestbook", "profile").'
2621
+ description: 'Firestore model type to resolve roles on (e.g. "guestbook", "profile").'
2144
2622
  },
2145
2623
  keys: {
2146
2624
  type: 'array',
2147
2625
  minItems: 1,
2626
+ maxItems: MCP_MODEL_ROLES_MAX_KEYS,
2148
2627
  description: 'Full keys ("prefix/id") or bare ids (root models only).',
2149
2628
  items: {
2150
2629
  type: 'string',
2151
2630
  minLength: 1
2152
2631
  }
2632
+ },
2633
+ uid: {
2634
+ type: 'string',
2635
+ minLength: 1,
2636
+ description: 'Resolve roles as this user instead of the caller. Requires elevated access; passing your own uid is always allowed.'
2153
2637
  }
2154
2638
  },
2155
2639
  additionalProperties: false
2156
2640
  };
2157
- var MODEL_GET_OUTPUT_SCHEMA = {
2641
+ var MODEL_ROLES_OUTPUT_SCHEMA = {
2158
2642
  type: 'object',
2159
2643
  required: [
2644
+ 'targeted',
2160
2645
  'results',
2161
2646
  'errors'
2162
2647
  ],
2163
2648
  properties: {
2649
+ uid: {
2650
+ type: 'string',
2651
+ description: 'The uid the roles were resolved for.'
2652
+ },
2653
+ targeted: {
2654
+ type: 'boolean',
2655
+ description: 'True when the roles were resolved for a user other than the caller.'
2656
+ },
2164
2657
  results: {
2165
2658
  type: 'array',
2166
- description: 'Per-key successful reads.',
2659
+ description: 'Per-key resolved permission state.',
2167
2660
  items: {
2168
2661
  type: 'object',
2169
2662
  required: [
2170
2663
  'key',
2171
- 'data'
2664
+ 'exists',
2665
+ 'fullAccess',
2666
+ 'roles'
2172
2667
  ],
2173
2668
  properties: {
2174
2669
  key: {
2175
2670
  type: 'string'
2176
2671
  },
2177
- data: {}
2672
+ exists: {
2673
+ type: 'boolean',
2674
+ description: 'Whether the document exists. Roles are only computed for documents that exist.'
2675
+ },
2676
+ fullAccess: {
2677
+ type: 'boolean',
2678
+ description: 'True when the full-access marker was granted; every role is implicitly held and "roles" is empty.'
2679
+ },
2680
+ roles: {
2681
+ type: 'array',
2682
+ items: {
2683
+ type: 'string'
2684
+ },
2685
+ description: 'Granted role names, sorted.'
2686
+ }
2178
2687
  }
2179
2688
  }
2180
2689
  },
2181
2690
  errors: {
2182
2691
  type: 'array',
2183
- description: 'Per-key failures (not-found, forbidden, etc.).',
2692
+ description: 'Per-key failures. A missing document is NOT an error — it appears in results with exists:false.',
2184
2693
  items: {
2185
2694
  type: 'object',
2186
2695
  required: [
@@ -2938,7 +3447,7 @@ var MODEL_ROW_SCHEMA = {
2938
3447
  }
2939
3448
  };
2940
3449
  // 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
3450
+ // (see the MCP SDK's `ToolSchema`), so the variants are expressed via a top-level
2942
3451
  // object whose nested `oneOf` discriminates by the `mode` literal.
2943
3452
  var MODEL_INFO_OUTPUT_SCHEMA = {
2944
3453
  type: 'object',
@@ -3740,7 +4249,7 @@ function whoamiToolHandler(args, ctx, deps) {
3740
4249
  }
3741
4250
  // Coerces the `detail` input into a WhoamiDetailLevel, defaulting unknown or absent values to `summary`.
3742
4251
  function parseDetailLevel(args) {
3743
- var value = args.detail;
4252
+ var value = args['detail'];
3744
4253
  return value === 'claims' || value === 'full' ? value : 'summary';
3745
4254
  }
3746
4255
  function buildResult(output, text) {
@@ -4335,30 +4844,30 @@ function urlModelsToolHandler(args, ctx, deps) {
4335
4844
  })();
4336
4845
  }
4337
4846
  function parseUrlModelsInput(args) {
4338
- var url = args.url;
4847
+ var url = args['url'];
4339
4848
  if (typeof url !== 'string' || url.length === 0) {
4340
4849
  throw new Error('url-models: "url" is required and must be a non-empty string.');
4341
4850
  }
4342
- var keysOnly = args.keysOnly === true;
4343
- var load = args.load === true;
4851
+ var keysOnly = args['keysOnly'] === true;
4852
+ var load = args['load'] === true;
4344
4853
  if (keysOnly && load) {
4345
4854
  throw new Error('url-models: "keysOnly" and "load" cannot be combined — "keysOnly" returns keys without loading documents.');
4346
4855
  }
4347
4856
  var models;
4348
- if (args.models !== undefined) {
4349
- if (!Array.isArray(args.models) || args.models.some(function(m) {
4857
+ if (args['models'] !== undefined) {
4858
+ if (!Array.isArray(args['models']) || args['models'].some(function(m) {
4350
4859
  return typeof m !== 'string';
4351
4860
  })) {
4352
4861
  throw new Error('url-models: "models" must be an array of model-type strings.');
4353
4862
  }
4354
- models = args.models;
4863
+ models = args['models'];
4355
4864
  }
4356
4865
  var currentUserUid;
4357
- if (args.currentUserUid !== undefined) {
4358
- if (typeof args.currentUserUid !== 'string' || args.currentUserUid.length === 0) {
4866
+ if (args['currentUserUid'] !== undefined) {
4867
+ if (typeof args['currentUserUid'] !== 'string' || args['currentUserUid'].length === 0) {
4359
4868
  throw new Error('url-models: "currentUserUid" must be a non-empty string.');
4360
4869
  }
4361
- currentUserUid = args.currentUserUid;
4870
+ currentUserUid = args['currentUserUid'];
4362
4871
  }
4363
4872
  return _object_spread$2(_object_spread_props$1(_object_spread$2({
4364
4873
  url: url
@@ -5306,19 +5815,19 @@ function batchExecuteToolHandler(args, ctx, deps) {
5306
5815
  // MARK: Input parsing
5307
5816
  function parseBatchExecuteInput(args) {
5308
5817
  var _args_format, _args_maxParallel;
5309
- var uploadPath = args.uploadPath;
5818
+ var uploadPath = args['uploadPath'];
5310
5819
  if (typeof uploadPath !== 'string' || uploadPath.length === 0) {
5311
5820
  throw new Error('batch-execute: "uploadPath" is required and must be a non-empty string.');
5312
5821
  }
5313
- var bucketId = args.bucketId;
5822
+ var bucketId = args['bucketId'];
5314
5823
  if (bucketId != null && (typeof bucketId !== 'string' || bucketId.length === 0)) {
5315
5824
  throw new Error('batch-execute: "bucketId" must be a non-empty string when provided.');
5316
5825
  }
5317
- var format = (_args_format = args.format) !== null && _args_format !== void 0 ? _args_format : 'ndjson';
5826
+ var format = (_args_format = args['format']) !== null && _args_format !== void 0 ? _args_format : 'ndjson';
5318
5827
  if (format !== 'json' && format !== 'ndjson') {
5319
5828
  throw new Error('batch-execute: "format" must be "json" or "ndjson".');
5320
5829
  }
5321
- var maxParallel = (_args_maxParallel = args.maxParallel) !== null && _args_maxParallel !== void 0 ? _args_maxParallel : DEFAULT_BATCH_EXECUTE_MAX_PARALLEL;
5830
+ var maxParallel = (_args_maxParallel = args['maxParallel']) !== null && _args_maxParallel !== void 0 ? _args_maxParallel : DEFAULT_BATCH_EXECUTE_MAX_PARALLEL;
5322
5831
  if (typeof maxParallel !== 'number' || !Number.isInteger(maxParallel) || maxParallel < 1) {
5323
5832
  throw new Error('batch-execute: "maxParallel" must be a positive integer.');
5324
5833
  }
@@ -5327,8 +5836,8 @@ function parseBatchExecuteInput(args) {
5327
5836
  bucketId: bucketId !== null && bucketId !== void 0 ? bucketId : undefined,
5328
5837
  format: format,
5329
5838
  maxParallel: maxParallel,
5330
- stopOnError: args.stopOnError === true,
5331
- deleteUploadOnSuccess: args.deleteUploadOnSuccess !== false
5839
+ stopOnError: args['stopOnError'] === true,
5840
+ deleteUploadOnSuccess: args['deleteUploadOnSuccess'] !== false
5332
5841
  };
5333
5842
  }
5334
5843
  function readOperationsFile(input) {
@@ -5409,9 +5918,9 @@ function validateOperation(value, index) {
5409
5918
  throw new Error("batch-execute: operation[".concat(index, "] must be an object."));
5410
5919
  }
5411
5920
  var candidate = value;
5412
- var modelType = candidate.modelType;
5413
- var call = candidate.call;
5414
- var specifier = candidate.specifier;
5921
+ var modelType = candidate['modelType'];
5922
+ var call = candidate['call'];
5923
+ var specifier = candidate['specifier'];
5415
5924
  if (typeof modelType !== 'string' || modelType.length === 0) {
5416
5925
  throw new Error("batch-execute: operation[".concat(index, '] is missing a non-empty "modelType".'));
5417
5926
  }
@@ -5425,7 +5934,7 @@ function validateOperation(value, index) {
5425
5934
  call: call,
5426
5935
  modelType: modelType,
5427
5936
  specifier: specifier !== null && specifier !== void 0 ? specifier : undefined,
5428
- data: candidate.data
5937
+ data: candidate['data']
5429
5938
  };
5430
5939
  }
5431
5940
  function collectPreflightErrors(operations, authorizeOperation) {
@@ -6053,17 +6562,19 @@ var McpServerFactoryService_1;
6053
6562
  * Injectable factory that builds {@link McpServer} instances pre-wired to the
6054
6563
  * call model dispatch chain.
6055
6564
  *
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.
6565
+ * The factory is invoked per Streamable HTTP request. That is the shape the MCP
6566
+ * SDK's 2026-07-28 entry (`createMcpHandler`) requires — it builds a fresh server
6567
+ * per request and has no session concept at all — and it is equally what the SDK
6568
+ * recommends for stateless 2025-era serving, so one factory backs both eras.
6059
6569
  */ var McpServerFactoryService = McpServerFactoryService_1 = /*#__PURE__*/ function() {
6060
- function McpServerFactoryService(mcpConfig, dispatchService, modelApiGetService, roleReader, analyticsService, storageService) {
6570
+ function McpServerFactoryService(mcpConfig, dispatchService, modelApiGetService, roleReader, analyticsService, storageService, modelRolesTargetUidPredicate) {
6061
6571
  _class_call_check$3(this, McpServerFactoryService);
6062
6572
  _define_property$3(this, "mcpConfig", void 0);
6063
6573
  _define_property$3(this, "dispatchService", void 0);
6064
6574
  _define_property$3(this, "modelApiGetService", void 0);
6065
6575
  _define_property$3(this, "roleReader", void 0);
6066
6576
  _define_property$3(this, "storageService", void 0);
6577
+ _define_property$3(this, "modelRolesTargetUidPredicate", void 0);
6067
6578
  _define_property$3(this, "_logger", new Logger(McpServerFactoryService_1.name));
6068
6579
  _define_property$3(this, "_cachedTools", void 0);
6069
6580
  _define_property$3(this, "_cachedStaticTools", void 0);
@@ -6083,6 +6594,7 @@ var McpServerFactoryService_1;
6083
6594
  this.modelApiGetService = modelApiGetService;
6084
6595
  this.roleReader = roleReader;
6085
6596
  this.storageService = storageService;
6597
+ this.modelRolesTargetUidPredicate = modelRolesTargetUidPredicate;
6086
6598
  this._analyticsService = analyticsService !== null && analyticsService !== void 0 ? analyticsService : noopMcpAnalyticsService();
6087
6599
  }
6088
6600
  _create_class$3(McpServerFactoryService, [
@@ -6133,7 +6645,7 @@ var McpServerFactoryService_1;
6133
6645
  batchTool
6134
6646
  ]);
6135
6647
  }
6136
- server.server.setRequestHandler(ListToolsRequestSchema, function() {
6648
+ server.server.setRequestHandler('tools/list', function() {
6137
6649
  return _async_to_generator$2(function() {
6138
6650
  var _this;
6139
6651
  return _ts_generator$2(this, function(_state) {
@@ -6141,6 +6653,10 @@ var McpServerFactoryService_1;
6141
6653
  return [
6142
6654
  2,
6143
6655
  {
6656
+ // McpToolListEntry types its schemas as opaque `object`: they are read from a manifest file
6657
+ // or produced by arktype's JSON Schema export, so the `type: 'object'` root the SDK's wire
6658
+ // type mandates holds at runtime but cannot be proven statically. Every schema reaching here
6659
+ // either came from arktypeToJsonSchemaForExport or defaulted to `{ type: 'object' }`.
6144
6660
  tools: listedTools.map(function(tool) {
6145
6661
  return _this._buildToolListEntry(tool, ctx, scopes);
6146
6662
  })
@@ -6149,7 +6665,7 @@ var McpServerFactoryService_1;
6149
6665
  });
6150
6666
  }).call(_this);
6151
6667
  });
6152
- server.server.setRequestHandler(CallToolRequestSchema, function(request) {
6668
+ server.server.setRequestHandler('tools/call', function(request) {
6153
6669
  return _async_to_generator$2(function() {
6154
6670
  return _ts_generator$2(this, function(_state) {
6155
6671
  return [
@@ -6349,6 +6865,14 @@ var McpServerFactoryService_1;
6349
6865
  resolveIdentity: function resolveIdentity(modelType, auth) {
6350
6866
  return getService.getModelIdentity(modelType, auth);
6351
6867
  }
6868
+ }), createModelRolesTool({
6869
+ readRoleMaps: function readRoleMaps(params) {
6870
+ return getService.readRoleMaps(params);
6871
+ },
6872
+ resolveIdentity: function resolveIdentity(modelType, auth) {
6873
+ return getService.getModelIdentity(modelType, auth);
6874
+ },
6875
+ canTargetOtherUids: this.modelRolesTargetUidPredicate
6352
6876
  }));
6353
6877
  // `url-models` reuses the model-get read/identity path, so it is only offered when the get
6354
6878
  // service is wired AND a route manifest with at least one state was loaded.
@@ -7102,7 +7626,9 @@ McpServerFactoryService = McpServerFactoryService_1 = __decorate([
7102
7626
  __param(4, Optional()),
7103
7627
  __param(4, Inject(MCP_ANALYTICS_SERVICE)),
7104
7628
  __param(5, Optional()),
7105
- __param(5, Inject(FirebaseServerStorageService))
7629
+ __param(5, Inject(FirebaseServerStorageService)),
7630
+ __param(6, Optional()),
7631
+ __param(6, Inject(MCP_MODEL_ROLES_TARGET_UID_PREDICATE))
7106
7632
  ], McpServerFactoryService);
7107
7633
 
7108
7634
  function asyncGeneratorStep$1(gen, resolve, reject, _next, _throw, key, arg) {
@@ -7234,29 +7760,168 @@ function _ts_generator$1(thisArg, body) {
7234
7760
  }
7235
7761
  }
7236
7762
  /**
7237
- * Handles a single Streamable HTTP JSON-RPC request against the supplied MCP server.
7763
+ * Handles a single Streamable HTTP JSON-RPC request, building a fresh MCP server for it.
7764
+ *
7765
+ * Both protocol eras are served, each by its own leg:
7238
7766
  *
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.
7767
+ * - **2026-07-28 (modern)** — `createMcpHandler`, which is per-request and stateless by
7768
+ * construction: there is no `Mcp-Session-Id` and no transport bookkeeping.
7769
+ * - **2025-era (legacy)** — a `NodeStreamableHTTPServerTransport` in stateless mode, which is
7770
+ * byte-for-byte what this endpoint served before the SDK v2 upgrade.
7771
+ *
7772
+ * The legacy leg is wired by hand (the SDK's documented `isLegacyRequest` composition) rather
7773
+ * than left to `createMcpHandler`'s built-in `legacy: 'stateless'` fallback, because that
7774
+ * fallback builds its transport with `sessionIdGenerator` alone and therefore answers in
7775
+ * `text/event-stream`. See {@link handleLegacyMcpRequest} for why that framing is not viable
7776
+ * here.
7243
7777
  *
7244
7778
  * The caller is expected to have already validated the bearer token (via the
7245
7779
  * OIDC bearer middleware) before this function runs.
7246
7780
  *
7247
7781
  * @param req - The Express request carrying the JSON-RPC body.
7248
7782
  * @param res - The Express response.
7783
+ * @param buildServer - Factory producing the MCP server that backs this request.
7784
+ */ function handleStreamableHttpMcpRequest(req, res, buildServer) {
7785
+ return _async_to_generator$1(function() {
7786
+ var probe;
7787
+ return _ts_generator$1(this, function(_state) {
7788
+ switch(_state.label){
7789
+ case 0:
7790
+ // Hand-wired compositions must reject non-JSON POST bodies themselves — neither the modern
7791
+ // entry's routing nor the legacy transport does it on our behalf once we classify up front.
7792
+ if (!isJsonContentType(req.headers['content-type'])) {
7793
+ res.status(415).json({
7794
+ statusCode: 415,
7795
+ message: 'Unsupported Media Type'
7796
+ });
7797
+ return [
7798
+ 2
7799
+ ];
7800
+ }
7801
+ return [
7802
+ 4,
7803
+ toWebRequest(req, req.body)
7804
+ ];
7805
+ case 1:
7806
+ probe = _state.sent();
7807
+ return [
7808
+ 4,
7809
+ isLegacyRequest(probe)
7810
+ ];
7811
+ case 2:
7812
+ if (!_state.sent()) return [
7813
+ 3,
7814
+ 4
7815
+ ];
7816
+ return [
7817
+ 4,
7818
+ handleLegacyMcpRequest(req, res, buildServer())
7819
+ ];
7820
+ case 3:
7821
+ _state.sent();
7822
+ return [
7823
+ 3,
7824
+ 6
7825
+ ];
7826
+ case 4:
7827
+ return [
7828
+ 4,
7829
+ handleModernMcpRequest(req, res, buildServer)
7830
+ ];
7831
+ case 5:
7832
+ _state.sent();
7833
+ _state.label = 6;
7834
+ case 6:
7835
+ return [
7836
+ 2
7837
+ ];
7838
+ }
7839
+ });
7840
+ })();
7841
+ }
7842
+ /**
7843
+ * Serves one 2026-07-28 request through `createMcpHandler`.
7844
+ *
7845
+ * `legacy: 'reject'` keeps this leg modern-only — 2025-era traffic never reaches it, having
7846
+ * already been routed to {@link handleLegacyMcpRequest}.
7847
+ *
7848
+ * `responseMode: 'json'` is the modern-leg equivalent of the legacy transport's
7849
+ * `enableJsonResponse`, keeping the endpoint on a single `application/json` body. Safe here
7850
+ * because no tool handler emits mid-call notifications, which are the only thing `'json'` drops.
7851
+ *
7852
+ * @param req - The Express request carrying the JSON-RPC body.
7853
+ * @param res - The Express response.
7854
+ * @param buildServer - Factory producing the MCP server that backs this request.
7855
+ */ function handleModernMcpRequest(req, res, buildServer) {
7856
+ return _async_to_generator$1(function() {
7857
+ var handler;
7858
+ return _ts_generator$1(this, function(_state) {
7859
+ switch(_state.label){
7860
+ case 0:
7861
+ handler = createMcpHandler(buildServer, {
7862
+ legacy: 'reject',
7863
+ responseMode: 'json'
7864
+ });
7865
+ _state.label = 1;
7866
+ case 1:
7867
+ _state.trys.push([
7868
+ 1,
7869
+ ,
7870
+ 3,
7871
+ 5
7872
+ ]);
7873
+ return [
7874
+ 4,
7875
+ toNodeHandler(handler)(req, res, req.body)
7876
+ ];
7877
+ case 2:
7878
+ _state.sent();
7879
+ return [
7880
+ 3,
7881
+ 5
7882
+ ];
7883
+ case 3:
7884
+ return [
7885
+ 4,
7886
+ handler.close()
7887
+ ];
7888
+ case 4:
7889
+ _state.sent();
7890
+ return [
7891
+ 7
7892
+ ];
7893
+ case 5:
7894
+ return [
7895
+ 2
7896
+ ];
7897
+ }
7898
+ });
7899
+ })();
7900
+ }
7901
+ /**
7902
+ * Serves one 2025-era request through a stateless `NodeStreamableHTTPServerTransport`.
7903
+ *
7904
+ * `sessionIdGenerator: undefined` selects stateless mode: the transport binds the server,
7905
+ * processes one request, and is discarded.
7906
+ *
7907
+ * `enableJsonResponse: true` replies with a single `application/json` body instead of framing it
7908
+ * as `text/event-stream`. Stateless mode answers exactly one JSON-RPC message per request and the
7909
+ * controller only implements POST (there is no standalone SSE stream to keep open), so SSE framing
7910
+ * adds nothing while making the endpoint fragile behind proxies that buffer or rewrite streamed
7911
+ * responses — the failure that originally pushed the demo app off its dev proxy.
7912
+ *
7913
+ * @param req - The Express request carrying the JSON-RPC body.
7914
+ * @param res - The Express response.
7249
7915
  * @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) {
7916
+ */ function handleLegacyMcpRequest(req, res, server) {
7252
7917
  return _async_to_generator$1(function() {
7253
7918
  var transport;
7254
7919
  return _ts_generator$1(this, function(_state) {
7255
7920
  switch(_state.label){
7256
7921
  case 0:
7257
- // sessionIdGenerator: undefined → stateless mode.
7258
- transport = new StreamableHTTPServerTransport({
7259
- sessionIdGenerator: undefined
7922
+ transport = new NodeStreamableHTTPServerTransport({
7923
+ sessionIdGenerator: undefined,
7924
+ enableJsonResponse: true
7260
7925
  });
7261
7926
  res.on('close', function() {
7262
7927
  void transport.close();
@@ -7448,9 +8113,11 @@ var McpController_1;
7448
8113
  * which must include `'/mcp'` in its `protectedPaths`. By the time the request reaches
7449
8114
  * this controller, `req.auth` is populated with the authenticated user's data.
7450
8115
  *
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.
8116
+ * Each request gets a fresh MCP server (stateless mode), which is adequate for Claude
8117
+ * custom-connector style usage. Both protocol eras are served — 2026-07-28 through the
8118
+ * SDK's per-request `createMcpHandler` entry, and 2025-era through a stateless Streamable
8119
+ * HTTP transport. A session-tracked variant can be layered on later if streaming tool
8120
+ * output becomes a requirement.
7454
8121
  */ var McpController = McpController_1 = /*#__PURE__*/ function() {
7455
8122
  function McpController(factory) {
7456
8123
  _class_call_check$2(this, McpController);
@@ -7463,15 +8130,12 @@ var McpController_1;
7463
8130
  key: "handleMcpRequest",
7464
8131
  value: function handleMcpRequest(req, res) {
7465
8132
  return _async_to_generator(function() {
7466
- var auth, server, error;
8133
+ var _this, auth, error;
7467
8134
  return _ts_generator(this, function(_state) {
7468
8135
  switch(_state.label){
7469
8136
  case 0:
8137
+ _this = this;
7470
8138
  auth = req.auth;
7471
- server = this.factory.createServer({
7472
- auth: auth,
7473
- rawRequest: req
7474
- });
7475
8139
  _state.label = 1;
7476
8140
  case 1:
7477
8141
  _state.trys.push([
@@ -7482,7 +8146,12 @@ var McpController_1;
7482
8146
  ]);
7483
8147
  return [
7484
8148
  4,
7485
- handleStreamableHttpMcpRequest(req, res, server)
8149
+ handleStreamableHttpMcpRequest(req, res, function() {
8150
+ return _this.factory.createServer({
8151
+ auth: auth,
8152
+ rawRequest: req
8153
+ });
8154
+ })
7486
8155
  ];
7487
8156
  case 2:
7488
8157
  _state.sent();
@@ -7511,6 +8180,45 @@ var McpController_1;
7511
8180
  });
7512
8181
  }).call(this);
7513
8182
  }
8183
+ },
8184
+ {
8185
+ /**
8186
+ * Rejects the Streamable HTTP transport's optional `GET` method, which opens a standalone SSE
8187
+ * stream. Not applicable in stateless mode. Separate from {@link handleUnsupportedDelete} because
8188
+ * NestJS binds one HTTP method per handler — stacking route decorators would silently drop one.
8189
+ *
8190
+ * @param res - The Express response to write the rejection to.
8191
+ */ key: "handleUnsupportedGet",
8192
+ value: function handleUnsupportedGet(res) {
8193
+ this._rejectUnsupportedMethod(res);
8194
+ }
8195
+ },
8196
+ {
8197
+ /**
8198
+ * Rejects the Streamable HTTP transport's optional `DELETE` method, which tears down a session.
8199
+ * Not applicable in stateless mode — no session is ever issued.
8200
+ *
8201
+ * @param res - The Express response to write the rejection to.
8202
+ */ key: "handleUnsupportedDelete",
8203
+ value: function handleUnsupportedDelete(res) {
8204
+ this._rejectUnsupportedMethod(res);
8205
+ }
8206
+ },
8207
+ {
8208
+ /**
8209
+ * Answers with a spec-conformant `405 Method Not Allowed` + `Allow` header. Without these
8210
+ * handlers NestJS answers `404`, which some clients treat as a hard failure rather than
8211
+ * "the server doesn't offer this".
8212
+ *
8213
+ * @param res - The Express response to write the rejection to.
8214
+ */ key: "_rejectUnsupportedMethod",
8215
+ value: function _rejectUnsupportedMethod(res) {
8216
+ res.setHeader('Allow', 'POST');
8217
+ res.status(HttpStatus.METHOD_NOT_ALLOWED).json({
8218
+ statusCode: HttpStatus.METHOD_NOT_ALLOWED,
8219
+ message: 'Method Not Allowed'
8220
+ });
8221
+ }
7514
8222
  }
7515
8223
  ]);
7516
8224
  return McpController;
@@ -7520,6 +8228,14 @@ __decorate([
7520
8228
  __param(0, Req()),
7521
8229
  __param(1, Res())
7522
8230
  ], McpController.prototype, "handleMcpRequest", null);
8231
+ __decorate([
8232
+ Get(),
8233
+ __param(0, Res())
8234
+ ], McpController.prototype, "handleUnsupportedGet", null);
8235
+ __decorate([
8236
+ Delete(),
8237
+ __param(0, Res())
8238
+ ], McpController.prototype, "handleUnsupportedDelete", null);
7523
8239
  McpController = McpController_1 = __decorate([
7524
8240
  Controller('mcp'),
7525
8241
  __param(0, Inject(McpServerFactoryService))
@@ -7605,6 +8321,13 @@ function _unsupported_iterable_to_array$2(o, minLen) {
7605
8321
  * URIs must live at the host root. Apps need to exclude `.well-known/{*path}`
7606
8322
  * from any global API route prefix (see `FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE`
7607
8323
  * in `firebase-server/oidc` for the corresponding OIDC excludes).
8324
+ *
8325
+ * Because the document must be reachable at the origin root, the MCP endpoint has to be
8326
+ * exposed through a host that serves the app at `/` — Firebase Hosting, or the hosting
8327
+ * emulator in development. Pointing a client straight at the Functions emulator origin
8328
+ * (`http://localhost:<port>/<project>/<region>/<function>/mcp`) breaks discovery: that
8329
+ * runtime only routes `/<project>/<region>/<function>/…`, so nothing answers at the root
8330
+ * and a client that has not yet seen a 401 challenge cannot find the issuer.
7608
8331
  */ var McpWellKnownController = /*#__PURE__*/ function() {
7609
8332
  function McpWellKnownController(mcpConfig, oidcProviderConfigService) {
7610
8333
  _class_call_check$1(this, McpWellKnownController);
@@ -7615,12 +8338,26 @@ function _unsupported_iterable_to_array$2(o, minLen) {
7615
8338
  }
7616
8339
  _create_class$1(McpWellKnownController, [
7617
8340
  {
7618
- key: "getProtectedResourceMetadata",
8341
+ /**
8342
+ * Serves both RFC 9728 discovery URLs — the primary form (§3.1), which inserts the resource's
8343
+ * path after the well-known prefix (`/.well-known/oauth-protected-resource/mcp`), and the bare
8344
+ * form (`/.well-known/oauth-protected-resource`) that clients probe as a fallback.
8345
+ *
8346
+ * The document is identical either way; the resource identity comes from
8347
+ * {@link McpModuleConfig.mcpUrl}, not from the request path. Serving the primary form means
8348
+ * discovery succeeds on a client's first probe rather than depending on it implementing the
8349
+ * fallback.
8350
+ *
8351
+ * @returns The protected-resource metadata document.
8352
+ */ key: "getProtectedResourceMetadata",
7619
8353
  value: function getProtectedResourceMetadata() {
7620
8354
  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;
8355
+ // Base scope list is what the issuer grants an arbitrary client, NOT its full
8356
+ // `scopes_supported`: a dynamic-registration client copies this list verbatim onto
8357
+ // `/authorize`, and a provider-profile-gated scope in it would be hard-rejected at the
8358
+ // consent unlock gate (`access_denied`) for every client lacking that profile assignment.
8359
+ // The optional filter narrows the list further.
8360
+ var providerScopes = this.oidcProviderConfigService.clientRequestableScopesSupported;
7624
8361
  var scopes = scopesFilter ? scopesFilter(providerScopes) : providerScopes;
7625
8362
  return _object_spread({
7626
8363
  resource: mcpUrl,
@@ -7636,7 +8373,10 @@ function _unsupported_iterable_to_array$2(o, minLen) {
7636
8373
  return McpWellKnownController;
7637
8374
  }();
7638
8375
  __decorate([
7639
- Get('oauth-protected-resource')
8376
+ Get([
8377
+ 'oauth-protected-resource',
8378
+ 'oauth-protected-resource/{*path}'
8379
+ ])
7640
8380
  ], McpWellKnownController.prototype, "getProtectedResourceMetadata", null);
7641
8381
  McpWellKnownController = __decorate([
7642
8382
  Controller('.well-known'),
@@ -7946,4 +8686,4 @@ function _unsupported_iterable_to_array(o, minLen) {
7946
8686
  };
7947
8687
  }
7948
8688
 
7949
- export { BATCH_EXECUTE_DISPATCH_CALL, BATCH_EXECUTE_DISPATCH_MODEL_TYPE, BATCH_EXECUTE_TOOL_NAME, DEFAULT_BATCH_EXECUTE_MAX_PARALLEL, DEFAULT_JSON_SCHEMA_GENERATION_OPTIONS, DEFAULT_MCP_ANALYTICS_EVENT_NAME, DEFAULT_MCP_PATH, DEFAULT_MCP_REASON_PARAMETER_DESCRIPTION, DEFAULT_MCP_REASON_PARAMETER_MAX_LENGTH, DEFAULT_MCP_REASON_PARAMETER_NAME, DEFAULT_MCP_REASON_PARAMETER_REQUIRED, DEFAULT_MCP_SERVER_INSTRUCTIONS, DEFAULT_MCP_SERVER_NAME, DEFAULT_SPECIFIER_KEY, DEFAULT_VOID_MCP_SUCCESS_VALUE, ENUM_INFO_DISPATCH_CALL, ENUM_INFO_DISPATCH_MODEL_TYPE, ENUM_INFO_TOOL_NAME, ENUM_TABLE_SCHEMA, FIREBASE_SERVER_MCP_ANALYTICS_CONFIG, FIREBASE_SERVER_MCP_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE, FirebaseServerMcpAnalyticsService, MAX_BATCH_EXECUTE_FILE_BYTES, MAX_BATCH_EXECUTE_OPERATIONS, MCP_ANALYTICS_SERVICE, MCP_AUTH_ROLE_READER, MCP_CALL_TYPE_ABBREVIATIONS, MCP_MANIFEST_VERSION, MCP_MODEL_GET_BATCH_SIZE, MCP_TOOL_NAME_MAX_LENGTH, MCP_TOOL_NAME_WARN_LENGTH, MCP_WRITE_TOOL_DESCRIPTION_PREFIX, MODEL_DECODE_DISPATCH_CALL, MODEL_DECODE_DISPATCH_MODEL_TYPE, MODEL_DECODE_TOOL_NAME, MODEL_GET_DISPATCH_CALL, MODEL_GET_DISPATCH_MODEL_TYPE, MODEL_GET_TOOL_NAME, MODEL_INFO_DISPATCH_CALL, MODEL_INFO_DISPATCH_MODEL_TYPE, MODEL_INFO_GROUPS_HINT, MODEL_INFO_TOOL_NAME, MODEL_INFO_UNGROUPED_LABEL, McpAnalyticsService, McpController, McpModuleConfig, McpServerFactoryService, McpWellKnownController, ROUTE_MANIFEST_VERSION, URL_MODELS_DISPATCH_CALL, URL_MODELS_DISPATCH_MODEL_TYPE, URL_MODELS_TOOL_NAME, abbreviateMcpCallType, appMcpAnalyticsModuleMetadata, applyMcpReasonParameterToSchema, applyWriteMarker, batchOperationCoordKey, buildDefaultMcpToolDescription, buildDisambiguatedMcpToolName, buildMcpToolName, buildStaticToolDefinition, buildStaticWireEntry, classifyVisibility, createBatchExecuteTool, createEnumInfoTool, createModelDecodeTool, createModelGetTool, createModelInfoTool, createUrlModelsTool, decodeFirestoreModelKey, extractMcpReasonFromArgs, findModelEntry, formatMcpToolErrorResponse, formatMcpToolResponse, generateMcpToolDefinitions, handleStreamableHttpMcpRequest, matchRouteManifestUrl, mcpManifestKey, mcpModuleMetadata, mcpSchemaDeclaresProperty, noopMcpAnalyticsService, parseUrlModelsPathname, readUrlModelsImpersonationUid, resolveEffectiveReadOnly, resolveEnumInfoOutput, resolveMcpReasonParameterConfig, resolveMcpToolAnnotations, resolveRequiredScope, validateMcpToolName };
8689
+ export { BATCH_EXECUTE_DISPATCH_CALL, BATCH_EXECUTE_DISPATCH_MODEL_TYPE, BATCH_EXECUTE_TOOL_NAME, DEFAULT_BATCH_EXECUTE_MAX_PARALLEL, DEFAULT_JSON_SCHEMA_GENERATION_OPTIONS, DEFAULT_MCP_ANALYTICS_EVENT_NAME, DEFAULT_MCP_PATH, DEFAULT_MCP_REASON_PARAMETER_DESCRIPTION, DEFAULT_MCP_REASON_PARAMETER_MAX_LENGTH, DEFAULT_MCP_REASON_PARAMETER_NAME, DEFAULT_MCP_REASON_PARAMETER_REQUIRED, DEFAULT_MCP_SERVER_INSTRUCTIONS, DEFAULT_MCP_SERVER_NAME, DEFAULT_SPECIFIER_KEY, DEFAULT_VOID_MCP_SUCCESS_VALUE, ENUM_INFO_DISPATCH_CALL, ENUM_INFO_DISPATCH_MODEL_TYPE, ENUM_INFO_TOOL_NAME, ENUM_TABLE_SCHEMA, FIREBASE_SERVER_MCP_ANALYTICS_CONFIG, FIREBASE_SERVER_MCP_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE, FirebaseServerMcpAnalyticsService, MAX_BATCH_EXECUTE_FILE_BYTES, MAX_BATCH_EXECUTE_OPERATIONS, MCP_ANALYTICS_SERVICE, MCP_AUTH_ROLE_READER, MCP_CALL_TYPE_ABBREVIATIONS, MCP_MANIFEST_VERSION, MCP_MODEL_GET_BATCH_SIZE, MCP_MODEL_ROLES_MAX_KEYS, MCP_MODEL_ROLES_TARGET_UID_PREDICATE, MCP_TOOL_NAME_MAX_LENGTH, MCP_TOOL_NAME_WARN_LENGTH, MCP_WRITE_TOOL_DESCRIPTION_PREFIX, MODEL_DECODE_DISPATCH_CALL, MODEL_DECODE_DISPATCH_MODEL_TYPE, MODEL_DECODE_TOOL_NAME, MODEL_GET_DISPATCH_CALL, MODEL_GET_DISPATCH_MODEL_TYPE, MODEL_GET_TOOL_NAME, MODEL_INFO_DISPATCH_CALL, MODEL_INFO_DISPATCH_MODEL_TYPE, MODEL_INFO_GROUPS_HINT, MODEL_INFO_TOOL_NAME, MODEL_INFO_UNGROUPED_LABEL, MODEL_ROLES_DISPATCH_CALL, MODEL_ROLES_DISPATCH_MODEL_TYPE, MODEL_ROLES_TOOL_NAME, McpAnalyticsService, McpController, McpModuleConfig, McpServerFactoryService, McpWellKnownController, ROUTE_MANIFEST_VERSION, URL_MODELS_DISPATCH_CALL, URL_MODELS_DISPATCH_MODEL_TYPE, URL_MODELS_TOOL_NAME, abbreviateMcpCallType, appMcpAnalyticsModuleMetadata, applyMcpReasonParameterToSchema, applyWriteMarker, batchOperationCoordKey, buildDefaultMcpToolDescription, buildDisambiguatedMcpToolName, buildMcpToolName, buildStaticToolDefinition, buildStaticWireEntry, classifyVisibility, createBatchExecuteTool, createEnumInfoTool, createModelDecodeTool, createModelGetTool, createModelInfoTool, createModelRolesTool, createUrlModelsTool, decodeFirestoreModelKey, extractMcpReasonFromArgs, findModelEntry, formatMcpToolErrorResponse, formatMcpToolResponse, generateMcpToolDefinitions, handleStreamableHttpMcpRequest, matchRouteManifestUrl, mcpManifestKey, mcpModuleMetadata, mcpSchemaDeclaresProperty, noopMcpAnalyticsService, parseUrlModelsPathname, readUrlModelsImpersonationUid, resolveEffectiveReadOnly, resolveEnumInfoOutput, resolveMcpReasonParameterConfig, resolveMcpToolAnnotations, resolveRequiredScope, validateMcpToolName };