@dereekb/firebase-server 13.28.0 → 13.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcp/index.cjs.js CHANGED
@@ -4,12 +4,12 @@ var common = require('@nestjs/common');
4
4
  var node_fs = require('node:fs');
5
5
  var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
6
6
  var types_js = require('@modelcontextprotocol/sdk/types.js');
7
+ var firebase = require('@dereekb/firebase');
7
8
  var oidc = require('@dereekb/firebase-server/oidc');
8
9
  var util = require('@dereekb/util');
9
10
  var firebaseServer = require('@dereekb/firebase-server');
10
11
  var node_util = require('node:util');
11
12
  var model = require('@dereekb/model');
12
- var firebase = require('@dereekb/firebase');
13
13
  var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
14
14
 
15
15
  function _class_call_check$5(instance, Constructor) {
@@ -167,6 +167,22 @@ function _define_property$9(obj, key, value) {
167
167
  * fills with a short justification for the call. The value is forwarded to analytics and stripped
168
168
  * from the dispatched handler body. See {@link McpReasonParameterConfig}.
169
169
  */ _define_property$9(this, "reasonParameter", void 0);
170
+ /**
171
+ * Default OIDC scope group term required on EVERY `callModel` op, mirrored into MCP tool-list
172
+ * visibility, unless a finer term overrides it (a per-function `requiredScope`, then a
173
+ * {@link McpModuleConfig.modelRequiredScopes} entry). Threaded into the same
174
+ * `resolveEffectiveOidcScopeTerms` composition the server-side model-api scope gate
175
+ * (`assertModelApiOidcScope`) uses, so a tool is advertised only when the caller could actually invoke it.
176
+ *
177
+ * Set this to the SAME value passed to `ModelApiDispatchConfig.defaultRequiredScope` to
178
+ * keep tool visibility and enforcement in lockstep.
179
+ */ _define_property$9(this, "defaultRequiredScope", void 0);
180
+ /**
181
+ * Per-model OIDC scope group-term overrides, keyed by {@link FirestoreModelType}, mirrored into MCP
182
+ * tool-list visibility. Set this to the SAME value passed to
183
+ * `ModelApiDispatchConfig.modelRequiredScopes` so per-model / verb-keyed restrictions that
184
+ * confine a subset client apply identically to the advertised tool list.
185
+ */ _define_property$9(this, "modelRequiredScopes", void 0);
170
186
  };
171
187
  /**
172
188
  * NestJS injection token for the optional {@link McpAuthRoleReader} provider.
@@ -1400,6 +1416,8 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1400
1416
  var seenNames = new Set();
1401
1417
  var manifest = context === null || context === void 0 ? void 0 : context.manifest;
1402
1418
  var naming = context === null || context === void 0 ? void 0 : context.naming;
1419
+ var defaultRequiredScope = context === null || context === void 0 ? void 0 : context.defaultRequiredScope;
1420
+ var modelRequiredScopes = context === null || context === void 0 ? void 0 : context.modelRequiredScopes;
1403
1421
  var candidates = planMcpToolCandidates(apiDetails, naming);
1404
1422
  var clashCounts = countVisibleAutoNameClashes(candidates);
1405
1423
  var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
@@ -1411,6 +1429,8 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1411
1429
  clashCounts: clashCounts,
1412
1430
  options: options,
1413
1431
  manifest: manifest,
1432
+ defaultRequiredScope: defaultRequiredScope,
1433
+ modelRequiredScopes: modelRequiredScopes,
1414
1434
  seenNames: seenNames,
1415
1435
  outTools: tools,
1416
1436
  outNeverVisibleTools: neverVisibleTools,
@@ -1580,9 +1600,9 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1580
1600
  *
1581
1601
  * @param context - The candidate plus the shared generation accumulators.
1582
1602
  */ function buildToolFromCandidate(context) {
1583
- var _clashCounts_get, _ref, _resolveRequiredScope;
1603
+ var _clashCounts_get, _ref;
1584
1604
  var _handlerDetails_mcp, _handlerDetails_mcp1;
1585
- var candidate = context.candidate, clashCounts = context.clashCounts, options = context.options, manifest = context.manifest, seenNames = context.seenNames, outTools = context.outTools, outNeverVisibleTools = context.outNeverVisibleTools, outSkipped = context.outSkipped, outWarnings = context.outWarnings;
1605
+ var candidate = context.candidate, clashCounts = context.clashCounts, options = context.options, manifest = context.manifest, defaultRequiredScope = context.defaultRequiredScope, modelRequiredScopes = context.modelRequiredScopes, seenNames = context.seenNames, outTools = context.outTools, outNeverVisibleTools = context.outNeverVisibleTools, outSkipped = context.outSkipped, outWarnings = context.outWarnings;
1586
1606
  var modelType = candidate.modelType, callType = candidate.callType, handlerDetails = candidate.handlerDetails, specifier = candidate.specifier, dispatch = candidate.dispatch, modelSegment = candidate.modelSegment, overrideName = candidate.overrideName, baseName = candidate.baseName, classified = candidate.classified, isVisible = candidate.isVisible;
1587
1607
  // A visible auto-named tool whose preferred name is produced by more than one visible tool is
1588
1608
  // re-derived with the abbreviated call type so both survive on the wire. Overrides and hidden tools
@@ -1632,7 +1652,18 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1632
1652
  });
1633
1653
  }
1634
1654
  }
1635
- var requiredScope = (_resolveRequiredScope = resolveRequiredScope(callType)) !== null && _resolveRequiredScope !== void 0 ? _resolveRequiredScope : undefined;
1655
+ // AND-of-ORs scope terms: the per-verb call-type scope (model.<call>) plus the effective group term,
1656
+ // resolved by the same precedence the server model-api scope gate uses (per-function requiredScope > per-model
1657
+ // requirement > module default) via the shared resolveEffectiveOidcScopeTerms — so tool visibility
1658
+ // tracks callability. Empty when no term applies (a non-CRUD call type with no per-function/model/
1659
+ // default term → no scope gate).
1660
+ var requiredScopeTerms = firebase.resolveEffectiveOidcScopeTerms({
1661
+ perVerbScope: resolveRequiredScope(callType),
1662
+ requiredScope: handlerDetails.requiredScope,
1663
+ modelRequirement: modelRequiredScopes === null || modelRequiredScopes === void 0 ? void 0 : modelRequiredScopes[modelType],
1664
+ call: callType,
1665
+ defaultRequiredScope: defaultRequiredScope
1666
+ });
1636
1667
  var effectiveReadOnly = resolveEffectiveReadOnly((_handlerDetails_mcp = handlerDetails.mcp) === null || _handlerDetails_mcp === void 0 ? void 0 : _handlerDetails_mcp.readOnly, callType);
1637
1668
  var annotations = resolveMcpToolAnnotations(effectiveReadOnly);
1638
1669
  var description = applyWriteMarker(baseDescription, annotations);
@@ -1641,20 +1672,20 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1641
1672
  filterMetadata = {
1642
1673
  visibilityKind: 'declarative',
1643
1674
  rule: classified.rule,
1644
- requiredScope: requiredScope,
1675
+ requiredScopeTerms: requiredScopeTerms,
1645
1676
  effectiveReadOnly: effectiveReadOnly
1646
1677
  };
1647
1678
  } else if (classified.visibilityKind === 'dynamic') {
1648
1679
  filterMetadata = {
1649
1680
  visibilityKind: 'dynamic',
1650
1681
  visibilityFn: classified.visibilityFn,
1651
- requiredScope: requiredScope,
1682
+ requiredScopeTerms: requiredScopeTerms,
1652
1683
  effectiveReadOnly: effectiveReadOnly
1653
1684
  };
1654
1685
  } else {
1655
1686
  filterMetadata = {
1656
1687
  visibilityKind: classified.visibilityKind,
1657
- requiredScope: requiredScope,
1688
+ requiredScopeTerms: requiredScopeTerms,
1658
1689
  effectiveReadOnly: effectiveReadOnly
1659
1690
  };
1660
1691
  }
@@ -6157,7 +6188,9 @@ var McpServerFactoryService_1;
6157
6188
  } else {
6158
6189
  result = generateMcpToolDefinitions(apiDetails, undefined, {
6159
6190
  manifest: manifest,
6160
- naming: this._resolveToolNamingOptions()
6191
+ naming: this._resolveToolNamingOptions(),
6192
+ defaultRequiredScope: this.mcpConfig.defaultRequiredScope,
6193
+ modelRequiredScopes: this.mcpConfig.modelRequiredScopes
6161
6194
  });
6162
6195
  }
6163
6196
  this._cachedTools = result;
@@ -6626,7 +6659,7 @@ var McpServerFactoryService_1;
6626
6659
  * Synthesizes the same `{ auth: { token } }` shape that `getOidcScopesFromRequest`
6627
6660
  * expects post-dispatch, so the upstream helper stays the single source of scope parsing.
6628
6661
  * Returns `undefined` for non-OIDC callers (no `oidcValidatedToken.scope`) — the filter
6629
- * loop treats that as "skip scope enforcement", matching `oidcCallModelScopePreAssert`.
6662
+ * loop treats that as "skip scope enforcement", matching the model-api scope gate (`assertModelApiOidcScope`).
6630
6663
  *
6631
6664
  * @param ctx - The per-request context carrying the validated auth payload.
6632
6665
  * @returns The set of granted OIDC scopes, or `undefined` when scope enforcement should be skipped.
@@ -6670,7 +6703,7 @@ var McpServerFactoryService_1;
6670
6703
  for(var _iterator = tools[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
6671
6704
  var tool = _step.value;
6672
6705
  var filterMetadata = tool.filterMetadata;
6673
- if (scopes != null && filterMetadata.requiredScope != null && !scopes.has(filterMetadata.requiredScope)) {
6706
+ if (scopes != null && filterMetadata.requiredScopeTerms != null && !firebase.oidcScopeTermsSatisfied(filterMetadata.requiredScopeTerms, scopes)) {
6674
6707
  continue;
6675
6708
  }
6676
6709
  if (readOnlyMode && filterMetadata.effectiveReadOnly !== true) {
package/mcp/index.esm.js CHANGED
@@ -2,12 +2,12 @@ import { Injectable, Inject, Optional, Logger, Post, Req, Res, Controller, Get }
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import { callModelOidcScopeForCallType, resolveEffectiveOidcScopeTerms, inferKeyFromTwoWayFlatFirestoreModelKey, oidcScopeTermsSatisfied } from '@dereekb/firebase';
5
6
  import { getOidcScopesFromRequest, OidcProviderConfigService } from '@dereekb/firebase-server/oidc';
6
7
  import { makeValuesGroupMap, IMPERSONATION_URL_QUERY_PARAM, performAsyncTasks, authRolesSetHasRoles } from '@dereekb/util';
7
8
  import { ModelApiCallModelDispatchService, ModelApiGetService, FirebaseServerStorageService, FirebaseServerEnvService, FirebaseServerAnalyticsService } from '@dereekb/firebase-server';
8
9
  import { inspect } from 'node:util';
9
10
  import { arktypeToJsonSchemaForExport } from '@dereekb/model';
10
- import { callModelOidcScopeForCallType, inferKeyFromTwoWayFlatFirestoreModelKey } from '@dereekb/firebase';
11
11
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
12
12
 
13
13
  function _class_call_check$5(instance, Constructor) {
@@ -165,6 +165,22 @@ function _define_property$9(obj, key, value) {
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
167
  */ _define_property$9(this, "reasonParameter", void 0);
168
+ /**
169
+ * Default OIDC scope group term required on EVERY `callModel` op, mirrored into MCP tool-list
170
+ * visibility, unless a finer term overrides it (a per-function `requiredScope`, then a
171
+ * {@link McpModuleConfig.modelRequiredScopes} entry). Threaded into the same
172
+ * `resolveEffectiveOidcScopeTerms` composition the server-side model-api scope gate
173
+ * (`assertModelApiOidcScope`) uses, so a tool is advertised only when the caller could actually invoke it.
174
+ *
175
+ * Set this to the SAME value passed to `ModelApiDispatchConfig.defaultRequiredScope` to
176
+ * keep tool visibility and enforcement in lockstep.
177
+ */ _define_property$9(this, "defaultRequiredScope", void 0);
178
+ /**
179
+ * Per-model OIDC scope group-term overrides, keyed by {@link FirestoreModelType}, mirrored into MCP
180
+ * tool-list visibility. Set this to the SAME value passed to
181
+ * `ModelApiDispatchConfig.modelRequiredScopes` so per-model / verb-keyed restrictions that
182
+ * confine a subset client apply identically to the advertised tool list.
183
+ */ _define_property$9(this, "modelRequiredScopes", void 0);
168
184
  };
169
185
  /**
170
186
  * NestJS injection token for the optional {@link McpAuthRoleReader} provider.
@@ -1398,6 +1414,8 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1398
1414
  var seenNames = new Set();
1399
1415
  var manifest = context === null || context === void 0 ? void 0 : context.manifest;
1400
1416
  var naming = context === null || context === void 0 ? void 0 : context.naming;
1417
+ var defaultRequiredScope = context === null || context === void 0 ? void 0 : context.defaultRequiredScope;
1418
+ var modelRequiredScopes = context === null || context === void 0 ? void 0 : context.modelRequiredScopes;
1401
1419
  var candidates = planMcpToolCandidates(apiDetails, naming);
1402
1420
  var clashCounts = countVisibleAutoNameClashes(candidates);
1403
1421
  var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
@@ -1409,6 +1427,8 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1409
1427
  clashCounts: clashCounts,
1410
1428
  options: options,
1411
1429
  manifest: manifest,
1430
+ defaultRequiredScope: defaultRequiredScope,
1431
+ modelRequiredScopes: modelRequiredScopes,
1412
1432
  seenNames: seenNames,
1413
1433
  outTools: tools,
1414
1434
  outNeverVisibleTools: neverVisibleTools,
@@ -1578,9 +1598,9 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1578
1598
  *
1579
1599
  * @param context - The candidate plus the shared generation accumulators.
1580
1600
  */ function buildToolFromCandidate(context) {
1581
- var _clashCounts_get, _ref, _resolveRequiredScope;
1601
+ var _clashCounts_get, _ref;
1582
1602
  var _handlerDetails_mcp, _handlerDetails_mcp1;
1583
- var candidate = context.candidate, clashCounts = context.clashCounts, options = context.options, manifest = context.manifest, seenNames = context.seenNames, outTools = context.outTools, outNeverVisibleTools = context.outNeverVisibleTools, outSkipped = context.outSkipped, outWarnings = context.outWarnings;
1603
+ var candidate = context.candidate, clashCounts = context.clashCounts, options = context.options, manifest = context.manifest, defaultRequiredScope = context.defaultRequiredScope, modelRequiredScopes = context.modelRequiredScopes, seenNames = context.seenNames, outTools = context.outTools, outNeverVisibleTools = context.outNeverVisibleTools, outSkipped = context.outSkipped, outWarnings = context.outWarnings;
1584
1604
  var modelType = candidate.modelType, callType = candidate.callType, handlerDetails = candidate.handlerDetails, specifier = candidate.specifier, dispatch = candidate.dispatch, modelSegment = candidate.modelSegment, overrideName = candidate.overrideName, baseName = candidate.baseName, classified = candidate.classified, isVisible = candidate.isVisible;
1585
1605
  // A visible auto-named tool whose preferred name is produced by more than one visible tool is
1586
1606
  // re-derived with the abbreviated call type so both survive on the wire. Overrides and hidden tools
@@ -1630,7 +1650,18 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1630
1650
  });
1631
1651
  }
1632
1652
  }
1633
- var requiredScope = (_resolveRequiredScope = resolveRequiredScope(callType)) !== null && _resolveRequiredScope !== void 0 ? _resolveRequiredScope : undefined;
1653
+ // AND-of-ORs scope terms: the per-verb call-type scope (model.<call>) plus the effective group term,
1654
+ // resolved by the same precedence the server model-api scope gate uses (per-function requiredScope > per-model
1655
+ // requirement > module default) via the shared resolveEffectiveOidcScopeTerms — so tool visibility
1656
+ // tracks callability. Empty when no term applies (a non-CRUD call type with no per-function/model/
1657
+ // default term → no scope gate).
1658
+ var requiredScopeTerms = resolveEffectiveOidcScopeTerms({
1659
+ perVerbScope: resolveRequiredScope(callType),
1660
+ requiredScope: handlerDetails.requiredScope,
1661
+ modelRequirement: modelRequiredScopes === null || modelRequiredScopes === void 0 ? void 0 : modelRequiredScopes[modelType],
1662
+ call: callType,
1663
+ defaultRequiredScope: defaultRequiredScope
1664
+ });
1634
1665
  var effectiveReadOnly = resolveEffectiveReadOnly((_handlerDetails_mcp = handlerDetails.mcp) === null || _handlerDetails_mcp === void 0 ? void 0 : _handlerDetails_mcp.readOnly, callType);
1635
1666
  var annotations = resolveMcpToolAnnotations(effectiveReadOnly);
1636
1667
  var description = applyWriteMarker(baseDescription, annotations);
@@ -1639,20 +1670,20 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1639
1670
  filterMetadata = {
1640
1671
  visibilityKind: 'declarative',
1641
1672
  rule: classified.rule,
1642
- requiredScope: requiredScope,
1673
+ requiredScopeTerms: requiredScopeTerms,
1643
1674
  effectiveReadOnly: effectiveReadOnly
1644
1675
  };
1645
1676
  } else if (classified.visibilityKind === 'dynamic') {
1646
1677
  filterMetadata = {
1647
1678
  visibilityKind: 'dynamic',
1648
1679
  visibilityFn: classified.visibilityFn,
1649
- requiredScope: requiredScope,
1680
+ requiredScopeTerms: requiredScopeTerms,
1650
1681
  effectiveReadOnly: effectiveReadOnly
1651
1682
  };
1652
1683
  } else {
1653
1684
  filterMetadata = {
1654
1685
  visibilityKind: classified.visibilityKind,
1655
- requiredScope: requiredScope,
1686
+ requiredScopeTerms: requiredScopeTerms,
1656
1687
  effectiveReadOnly: effectiveReadOnly
1657
1688
  };
1658
1689
  }
@@ -6155,7 +6186,9 @@ var McpServerFactoryService_1;
6155
6186
  } else {
6156
6187
  result = generateMcpToolDefinitions(apiDetails, undefined, {
6157
6188
  manifest: manifest,
6158
- naming: this._resolveToolNamingOptions()
6189
+ naming: this._resolveToolNamingOptions(),
6190
+ defaultRequiredScope: this.mcpConfig.defaultRequiredScope,
6191
+ modelRequiredScopes: this.mcpConfig.modelRequiredScopes
6159
6192
  });
6160
6193
  }
6161
6194
  this._cachedTools = result;
@@ -6624,7 +6657,7 @@ var McpServerFactoryService_1;
6624
6657
  * Synthesizes the same `{ auth: { token } }` shape that `getOidcScopesFromRequest`
6625
6658
  * expects post-dispatch, so the upstream helper stays the single source of scope parsing.
6626
6659
  * Returns `undefined` for non-OIDC callers (no `oidcValidatedToken.scope`) — the filter
6627
- * loop treats that as "skip scope enforcement", matching `oidcCallModelScopePreAssert`.
6660
+ * loop treats that as "skip scope enforcement", matching the model-api scope gate (`assertModelApiOidcScope`).
6628
6661
  *
6629
6662
  * @param ctx - The per-request context carrying the validated auth payload.
6630
6663
  * @returns The set of granted OIDC scopes, or `undefined` when scope enforcement should be skipped.
@@ -6668,7 +6701,7 @@ var McpServerFactoryService_1;
6668
6701
  for(var _iterator = tools[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
6669
6702
  var tool = _step.value;
6670
6703
  var filterMetadata = tool.filterMetadata;
6671
- if (scopes != null && filterMetadata.requiredScope != null && !scopes.has(filterMetadata.requiredScope)) {
6704
+ if (scopes != null && filterMetadata.requiredScopeTerms != null && !oidcScopeTermsSatisfied(filterMetadata.requiredScopeTerms, scopes)) {
6672
6705
  continue;
6673
6706
  }
6674
6707
  if (readOnlyMode && filterMetadata.effectiveReadOnly !== true) {
package/mcp/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/mcp",
3
- "version": "13.28.0",
3
+ "version": "13.30.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.28.0",
6
- "@dereekb/date": "13.28.0",
7
- "@dereekb/firebase": "13.28.0",
8
- "@dereekb/firebase-server": "13.28.0",
9
- "@dereekb/firebase-server/oidc": "13.28.0",
10
- "@dereekb/model": "13.28.0",
11
- "@dereekb/nestjs": "13.28.0",
12
- "@dereekb/rxjs": "13.28.0",
13
- "@dereekb/util": "13.28.0",
14
- "@dereekb/zoho": "13.28.0",
5
+ "@dereekb/analytics": "13.30.0",
6
+ "@dereekb/date": "13.30.0",
7
+ "@dereekb/firebase": "13.30.0",
8
+ "@dereekb/firebase-server": "13.30.0",
9
+ "@dereekb/firebase-server/oidc": "13.30.0",
10
+ "@dereekb/model": "13.30.0",
11
+ "@dereekb/nestjs": "13.30.0",
12
+ "@dereekb/rxjs": "13.30.0",
13
+ "@dereekb/util": "13.30.0",
14
+ "@dereekb/zoho": "13.30.0",
15
15
  "@modelcontextprotocol/sdk": "1.29.0",
16
16
  "@nestjs/common": "^11.1.19",
17
17
  "@nestjs/core": "^11.1.19",
@@ -1,5 +1,5 @@
1
1
  import { type AuthClaims, type AuthRoleSet } from '@dereekb/util';
2
- import { type OidcScope } from '@dereekb/firebase';
2
+ import { type FirestoreModelType, type OidcModelScopeRequirement, type OidcScope, type OidcScopeTerm } from '@dereekb/firebase';
3
3
  /**
4
4
  * Default path the MCP Streamable HTTP transport is mounted at.
5
5
  *
@@ -200,6 +200,24 @@ export declare abstract class McpModuleConfig {
200
200
  * from the dispatched handler body. See {@link McpReasonParameterConfig}.
201
201
  */
202
202
  readonly reasonParameter?: McpReasonParameterConfig | boolean;
203
+ /**
204
+ * Default OIDC scope group term required on EVERY `callModel` op, mirrored into MCP tool-list
205
+ * visibility, unless a finer term overrides it (a per-function `requiredScope`, then a
206
+ * {@link McpModuleConfig.modelRequiredScopes} entry). Threaded into the same
207
+ * `resolveEffectiveOidcScopeTerms` composition the server-side model-api scope gate
208
+ * (`assertModelApiOidcScope`) uses, so a tool is advertised only when the caller could actually invoke it.
209
+ *
210
+ * Set this to the SAME value passed to `ModelApiDispatchConfig.defaultRequiredScope` to
211
+ * keep tool visibility and enforcement in lockstep.
212
+ */
213
+ readonly defaultRequiredScope?: OidcScopeTerm;
214
+ /**
215
+ * Per-model OIDC scope group-term overrides, keyed by {@link FirestoreModelType}, mirrored into MCP
216
+ * tool-list visibility. Set this to the SAME value passed to
217
+ * `ModelApiDispatchConfig.modelRequiredScopes` so per-model / verb-keyed restrictions that
218
+ * confine a subset client apply identically to the advertised tool list.
219
+ */
220
+ readonly modelRequiredScopes?: Record<FirestoreModelType, OidcModelScopeRequirement>;
203
221
  }
204
222
  /**
205
223
  * Signature for the optional role reader the MCP module uses when evaluating
@@ -152,7 +152,7 @@ export declare class McpServerFactoryService {
152
152
  * Synthesizes the same `{ auth: { token } }` shape that `getOidcScopesFromRequest`
153
153
  * expects post-dispatch, so the upstream helper stays the single source of scope parsing.
154
154
  * Returns `undefined` for non-OIDC callers (no `oidcValidatedToken.scope`) — the filter
155
- * loop treats that as "skip scope enforcement", matching `oidcCallModelScopePreAssert`.
155
+ * loop treats that as "skip scope enforcement", matching the model-api scope gate (`assertModelApiOidcScope`).
156
156
  *
157
157
  * @param ctx - The per-request context carrying the validated auth payload.
158
158
  * @returns The set of granted OIDC scopes, or `undefined` when scope enforcement should be skipped.
@@ -1,5 +1,5 @@
1
1
  import { type Maybe } from '@dereekb/util';
2
- import { type KnownOnCallFunctionType } from '@dereekb/firebase';
2
+ import { type FirestoreModelType, type KnownOnCallFunctionType, type OidcModelScopeRequirement, type OidcScopeTerm } from '@dereekb/firebase';
3
3
  import { type ModelApiDetailsResult, type OnCallModelFunctionApiDetails, type FirebaseServerAuthData, type McpToolDetailsBuilder, type McpVisibilityRule } from '@dereekb/firebase-server';
4
4
  import { type Request } from 'express';
5
5
  import { type CallToolResult, type ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
@@ -452,6 +452,17 @@ export interface GenerateMcpToolDefinitionsContext {
452
452
  * Per-model name segment overrides (e.g. collection prefixes).
453
453
  */
454
454
  readonly naming?: McpToolGenerationNamingOptions;
455
+ /**
456
+ * Default OIDC scope group term required on every op unless a finer term overrides it. Mirrors the
457
+ * model-api layer's `ModelApiDispatchConfig.defaultRequiredScope` so a tool is advertised only when
458
+ * the caller could invoke it.
459
+ */
460
+ readonly defaultRequiredScope?: OidcScopeTerm;
461
+ /**
462
+ * Per-model OIDC scope group-term overrides, keyed by {@link FirestoreModelType}. Mirrors the model-api
463
+ * layer's `ModelApiDispatchConfig.modelRequiredScopes` so per-model / verb-keyed restrictions apply to tool visibility.
464
+ */
465
+ readonly modelRequiredScopes?: Record<FirestoreModelType, OidcModelScopeRequirement>;
455
466
  }
456
467
  /**
457
468
  * Generates MCP tool definitions from a model-first API details tree.
@@ -1,5 +1,5 @@
1
1
  import { type Maybe } from '@dereekb/util';
2
- import { type CallModelOidcScope } from '@dereekb/firebase';
2
+ import { type CallModelOidcScope, type OidcScopeTerm } from '@dereekb/firebase';
3
3
  import { type McpToolVisibility, type McpVisibilityContext, type McpVisibilityRule } from '@dereekb/firebase-server';
4
4
  import { type ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
5
5
  /**
@@ -41,10 +41,15 @@ export interface ClassifiedMcpToolVisibilityDynamic {
41
41
  export type McpToolFilterMetadata = McpToolFilterMetadataAlways | McpToolFilterMetadataNever | McpToolFilterMetadataDeclarative | McpToolFilterMetadataDynamic;
42
42
  interface McpToolFilterMetadataBase {
43
43
  /**
44
- * OIDC scope required to invoke this tool. Precomputed from the dispatch call type.
45
- * `undefined` for non-CRUD call types (apps gate those via their own preAssert if needed).
44
+ * OIDC scope TERMS required to invoke this tool, evaluated AND-of-ORs — the caller must satisfy
45
+ * EVERY term, and an array term (OR-group) is satisfied by holding ANY member. Checked with the
46
+ * shared `oidcScopeTermsSatisfied` so tool-list visibility matches the server-side `callModel` gate
47
+ * exactly. Precomputed from the dispatch call type's per-verb scope (`model.<call>`) plus the
48
+ * effective group term (per-function `requiredScope` > per-model requirement > module default).
49
+ * Empty when no scope is enforced (a non-CRUD call type with no per-function/model/default term);
50
+ * apps gate custom verbs via their own preAssert.
46
51
  */
47
- readonly requiredScope?: CallModelOidcScope;
52
+ readonly requiredScopeTerms?: readonly OidcScopeTerm[];
48
53
  /**
49
54
  * Effective read-only classification used by the module-level `readOnly` filter.
50
55
  *
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/model",
3
- "version": "13.28.0",
3
+ "version": "13.30.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.28.0",
6
- "@dereekb/date": "13.28.0",
7
- "@dereekb/firebase": "13.28.0",
8
- "@dereekb/firebase-server": "13.28.0",
9
- "@dereekb/model": "13.28.0",
10
- "@dereekb/nestjs": "13.28.0",
11
- "@dereekb/rxjs": "13.28.0",
12
- "@dereekb/util": "13.28.0",
5
+ "@dereekb/analytics": "13.30.0",
6
+ "@dereekb/date": "13.30.0",
7
+ "@dereekb/firebase": "13.30.0",
8
+ "@dereekb/firebase-server": "13.30.0",
9
+ "@dereekb/model": "13.30.0",
10
+ "@dereekb/nestjs": "13.30.0",
11
+ "@dereekb/rxjs": "13.30.0",
12
+ "@dereekb/util": "13.30.0",
13
13
  "@nestjs/common": "^11.1.19",
14
14
  "@nestjs/config": "^4.0.4",
15
15
  "archiver": "^7.0.1",