@happyvertical/smrt-app-mcp 0.53.4 → 0.53.6

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/AGENTS.md CHANGED
@@ -34,6 +34,17 @@ tool policy; `./sveltekit` owns the stateless Streamable HTTP transport.
34
34
  re-check the originating action against the allow-list and `effects`,
35
35
  after a principal-level `taskPrincipalPolicy` (defaults: scope policy);
36
36
  `mountMcpProtectedResourceMetadataRoute` serves only the advertised path.
37
+ - `createHostedMcpResourceAuth({ profile, runtime })`: locally, a runtime
38
+ `verifyLocalMcpToken` yields the `sessionFallback` adapter
39
+ (`createLocalMcpTokenAuth`): no `Authorization` header keeps the session
40
+ principal; a presented bearer must verify (401, never cookie fallback); no
41
+ metadata (404). Hosted, `runtime.resolveMcpPrincipal` is the default
42
+ identity mapping; an explicit `resolvePrincipal` overrides it.
43
+ - Adapters stamp `principal.tenantBinding` for `bindPrincipal`: the local
44
+ token adapter always `direct`; the JWT adapter the mapping's value or
45
+ `direct-or-inherited`. Never derive it from request input. Both adapters
46
+ read principals/mappings from own data only (`own-data.ts`): inherited
47
+ values, accessors and poison keys deny.
37
48
  - Keep tool catalogs private by default. Public caching requires an explicit
38
49
  attestation and a global, unauthenticated, read-only, non-tenant catalog with
39
50
  no principal-aware policy.
package/README.md CHANGED
@@ -126,10 +126,10 @@ binding. `createHostedMcpResourceAuth` from `./auth` builds that source from
126
126
 
127
127
  ```ts
128
128
  import { createHostedMcpResourceAuth } from '@happyvertical/smrt-app-mcp/auth';
129
- const auth = createHostedMcpResourceAuth({
130
- profile: () => applicationRuntime.profile,
131
- resolvePrincipal: resolveHostedMcpPrincipal, // application-owned lookup
132
- });
129
+ const { profile } = await runtime.resolvedRuntime();
130
+ // `runtime` supplies both credential bindings (see below); pass
131
+ // `resolvePrincipal` as well to override the hosted identity mapping.
132
+ const auth = createHostedMcpResourceAuth({ profile, runtime });
133
133
  // api/mcp/+server.ts
134
134
  export const POST = mountMcpAppRoute({
135
135
  models: [Item], requiredScopes: ['items.read'], effects: ['read'],
@@ -139,9 +139,28 @@ export const POST = mountMcpAppRoute({
139
139
  export const GET = mountMcpProtectedResourceMetadataRoute(auth);
140
140
  ```
141
141
 
142
+ The `runtime` option takes any object with the optional
143
+ `verifyLocalMcpToken(token)` and `resolveMcpPrincipal(identity)` methods; the
144
+ s-m-r-t SvelteKit runtime has both.
145
+
146
+ - **Local profile.** With `verifyLocalMcpToken`, the source yields the
147
+ `createLocalMcpTokenAuth` adapter for owner-minted tokens
148
+ (`smrt app token --scopes items.read`). A request with no `Authorization`
149
+ header keeps the session principal, exactly as before. A presented bearer
150
+ must verify: an unknown, revoked or expired token is a bare 401
151
+ (`WWW-Authenticate: Bearer error="invalid_token"`) and never falls back to
152
+ the cookie. The origin check still runs first. Without the runtime the
153
+ source yields `null`, as before.
154
+ - **Hosted profiles.** Without `resolvePrincipal`, the runtime's
155
+ membership-backed `resolveMcpPrincipal` maps the verified issuer/subject to
156
+ a user linked through `oidc_identities` with exactly one active tenant
157
+ membership, and denies anything else. Pass `resolvePrincipal` when users
158
+ belong to several tenants.
159
+
142
160
  `mountMcpProtectedResourceMetadataRoute` serves the RFC 9728 document only at
143
- the adapter's advertised `metadataUrl` path and returns 404 otherwise or for
144
- the `local` profile. It does not implement an OAuth authorization server.
161
+ the adapter's advertised `metadataUrl` path. It returns 404 for any other
162
+ path, and in the `local` profile (no adapter, or the local token adapter). It
163
+ does not implement an OAuth authorization server.
145
164
 
146
165
  For piping a deployed app's MCP surface to a local stdio MCP client, see `@happyvertical/smrt-app-cli` — the client-side runtime CLI exposes a `startMcpBridge()` default and a generic `smrt-mcp-bridge` bin.
147
166
 
package/dist/auth.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Server-only OAuth protected-resource boundary. This is not an issuer. */
2
2
  import { type JWTPayload } from 'jose';
3
- import type { McpAppPrincipal } from './server.js';
3
+ import type { McpAppPrincipal, McpTenantBinding } from './server.js';
4
4
  export type McpDeploymentProfile = 'local' | 'self-hosted' | 'cloud';
5
5
  export interface McpVerifiedIdentity {
6
6
  readonly issuer: string;
@@ -13,6 +13,12 @@ export interface McpPrincipalMapping {
13
13
  tenantId?: string;
14
14
  kind?: string;
15
15
  roles?: string[];
16
+ /**
17
+ * How the mapped tenant authority may be bound. Defaults to
18
+ * `direct-or-inherited`; a mapping that selected a direct membership
19
+ * returns `direct` so inheritance can never substitute for it.
20
+ */
21
+ tenantBinding?: McpTenantBinding;
16
22
  }
17
23
  export interface McpResourceAuthOptions {
18
24
  profile: McpDeploymentProfile;
@@ -35,9 +41,21 @@ export interface McpResourceAuthOptions {
35
41
  resolvePrincipal(identity: McpVerifiedIdentity): Promise<McpPrincipalMapping | null>;
36
42
  }
37
43
  export interface McpResourceAuth {
38
- /** Mount GET at this URL. Paths use RFC 9728's well-known path insertion. */
44
+ /**
45
+ * Mount GET at this URL. Paths use RFC 9728's well-known path insertion.
46
+ * Empty for a {@link McpResourceAuth.sessionFallback} adapter, which
47
+ * advertises no OAuth metadata.
48
+ */
39
49
  readonly metadataUrl: string;
40
50
  metadataResponse(): Response;
51
+ /**
52
+ * When `true`, a request that carries no `Authorization` header is not
53
+ * challenged: the route resolves its session principal instead (today's
54
+ * local-profile behaviour). A presented bearer must still verify; an
55
+ * invalid one is refused and never falls back to the session. Only the
56
+ * local owner-token adapter ({@link createLocalMcpTokenAuth}) sets it.
57
+ */
58
+ readonly sessionFallback?: boolean;
41
59
  /** A failure response contains no token, claims or upstream error details. */
42
60
  authenticate(request: Request): Promise<{
43
61
  ok: true;
@@ -49,6 +67,51 @@ export interface McpResourceAuth {
49
67
  response: Response;
50
68
  }>;
51
69
  }
70
+ /**
71
+ * Credential bindings an application runtime can supply to
72
+ * {@link createHostedMcpResourceAuth}, structurally satisfied by the SMRT
73
+ * SvelteKit runtime from `@happyvertical/smrt-app-runtime/sveltekit`.
74
+ */
75
+ /**
76
+ * A verified local-token principal as a runtime returns it. Its `scopes` may
77
+ * be a frozen (readonly) array; the adapter copies it.
78
+ */
79
+ export type McpLocalTokenPrincipal = Omit<McpAppPrincipal, 'scopes'> & {
80
+ id: string;
81
+ scopes?: readonly string[];
82
+ };
83
+ export interface McpAuthRuntime {
84
+ /**
85
+ * Verify an owner-minted local MCP token (`smrt app token`). Resolve the
86
+ * token principal (with scopes already capped to live permissions), or
87
+ * `null` to deny. Used only in the `local` profile.
88
+ */
89
+ verifyLocalMcpToken?(token: string): Promise<McpLocalTokenPrincipal | null>;
90
+ /**
91
+ * Default hosted identity mapping, used when no explicit
92
+ * `resolvePrincipal` is given (for example the runtime's membership-backed
93
+ * resolver).
94
+ */
95
+ resolveMcpPrincipal?(identity: McpVerifiedIdentity): Promise<McpPrincipalMapping | null>;
96
+ }
97
+ /** Options for {@link createLocalMcpTokenAuth}. */
98
+ export interface LocalMcpTokenAuthOptions {
99
+ /**
100
+ * Resolve a presented token to its principal, or `null` to deny. A thrown
101
+ * error is also a denial. The principal needs a non-empty `id` and
102
+ * `tenantId` and string `scopes`; `allowCrossTenant` is never honoured.
103
+ */
104
+ verify(token: string): Promise<McpLocalTokenPrincipal | null>;
105
+ }
106
+ /**
107
+ * Bearer adapter for owner-minted local MCP tokens. It is a
108
+ * {@link McpResourceAuth.sessionFallback} adapter: without an `Authorization`
109
+ * header the route keeps its session principal; with one, the token must
110
+ * verify, otherwise the response is `401` with
111
+ * `WWW-Authenticate: Bearer error="invalid_token"` and no token, claim or
112
+ * upstream detail. It advertises no OAuth metadata (the metadata route 404s).
113
+ */
114
+ export declare function createLocalMcpTokenAuth(options: LocalMcpTokenAuthOptions): McpResourceAuth;
52
115
  /** Options for {@link createHostedMcpResourceAuth}. */
53
116
  export interface HostedMcpResourceAuthOptions {
54
117
  /**
@@ -58,10 +121,20 @@ export interface HostedMcpResourceAuthOptions {
58
121
  profile: McpDeploymentProfile | (() => McpDeploymentProfile);
59
122
  /**
60
123
  * Application-owned mapping from a verified identity to a current account
61
- * and active membership. Required for every non-local profile; token claims
62
- * never select a tenant. An absent binding throws on first hosted use.
124
+ * and active membership; token claims never select a tenant. Overrides
125
+ * `runtime.resolveMcpPrincipal`. A hosted profile with neither throws on
126
+ * first use.
63
127
  */
64
128
  resolvePrincipal?: McpResourceAuthOptions['resolvePrincipal'];
129
+ /**
130
+ * The application runtime (for example the SMRT SvelteKit runtime). In the
131
+ * `local` profile its `verifyLocalMcpToken` turns on owner-minted bearer
132
+ * tokens through {@link createLocalMcpTokenAuth} (requests without a
133
+ * bearer keep the session principal); without it the thunk yields `null`
134
+ * as before. In hosted profiles its membership-backed
135
+ * `resolveMcpPrincipal` is the default identity mapping.
136
+ */
137
+ runtime?: McpAuthRuntime;
65
138
  /** Environment source. Defaults to `process.env`, read lazily. */
66
139
  env?: Readonly<Record<string, string | undefined>>;
67
140
  /** Default `['RS256']`. */
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,OAAO,EAAsB,KAAK,UAAU,EAAa,MAAM,MAAM,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,aAAa,GAAG,OAAO,CAAC;AACrE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;CACvC;AACD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AACD,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,UAAU,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC;IAC3C,qFAAqF;IACrF,SAAS,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;IAC7B,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,gBAAgB,CACd,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;CACxC;AAqCD,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gBAAgB,IAAI,QAAQ,CAAC;IAC7B,8EAA8E;IAC9E,YAAY,CACV,OAAO,EAAE,OAAO,GACf,OAAO,CACN;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,eAAe,GAAG;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GACzD;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,QAAQ,EAAE,QAAQ,CAAA;KAAE,CACpC,CAAC;CACH;AAED,uDAAuD;AACvD,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,OAAO,EAAE,oBAAoB,GAAG,CAAC,MAAM,oBAAoB,CAAC,CAAC;IAC7D;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,sBAAsB,CAAC,kBAAkB,CAAC,CAAC;IAC9D,kEAAkE;IAClE,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IACnD,2BAA2B;IAC3B,UAAU,CAAC,EAAE,sBAAsB,CAAC,YAAY,CAAC,CAAC;IAClD,SAAS,CAAC,EAAE,sBAAsB,CAAC,WAAW,CAAC,CAAC;IAChD,aAAa,CAAC,EAAE,sBAAsB,CAAC,eAAe,CAAC,CAAC;CACzD;AAED,yEAAyE;AACzE,eAAO,MAAM,mBAAmB;;;;;EAK9B,CAAC;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,4BAA4B,GACpC,MAAM,eAAe,GAAG,IAAI,CAqC9B;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CA0HjB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/auth.js CHANGED
@@ -697,7 +697,44 @@ function createRemoteJWKSet(url, options) {
697
697
  });
698
698
  }
699
699
  //#endregion
700
+ //#region src/own-data.ts
701
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
702
+ "__proto__",
703
+ "constructor",
704
+ "prototype"
705
+ ]);
706
+ function readOwnFields(value, keys) {
707
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
708
+ const prototype = Object.getPrototypeOf(value);
709
+ if (prototype !== Object.prototype && prototype !== null) return null;
710
+ for (const key of Reflect.ownKeys(value)) if (typeof key === "string" && FORBIDDEN_KEYS.has(key)) return null;
711
+ const fields = /* @__PURE__ */ Object.create(null);
712
+ for (const key of keys) {
713
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
714
+ if (!descriptor) continue;
715
+ if (!("value" in descriptor)) return null;
716
+ fields[key] = descriptor.value;
717
+ }
718
+ return fields;
719
+ }
720
+ function ownStringArray(value) {
721
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) return null;
722
+ const length = value.length;
723
+ for (const key of Reflect.ownKeys(value)) {
724
+ if (key === "length") continue;
725
+ if (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length) return null;
726
+ }
727
+ const copy = [];
728
+ for (let index = 0; index < length; index += 1) {
729
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, index);
730
+ if (!descriptor || !("value" in descriptor) || typeof descriptor.value !== "string") return null;
731
+ copy.push(descriptor.value);
732
+ }
733
+ return copy;
734
+ }
735
+ //#endregion
700
736
  //#region src/auth.ts
737
+ var TENANT_BINDINGS = ["direct", "direct-or-inherited"];
701
738
  var scopePattern = /^[\x21\x23-\x5b\x5d-\x7e]+$/u;
702
739
  function validateUrl(value, profile) {
703
740
  let url;
@@ -717,6 +754,57 @@ function validateUrl(value, profile) {
717
754
  function validString(value) {
718
755
  return typeof value === "string" && value.length > 0 && value.length <= 1024 && value.trim() === value && [...value].every((character) => character.charCodeAt(0) > 31 && character.charCodeAt(0) !== 127);
719
756
  }
757
+ var LOCAL_BEARER_PATTERN = /^Bearer +([A-Za-z0-9._~+/-]{16,512}=*)$/iu;
758
+ function createLocalMcpTokenAuth(options) {
759
+ const verify = options?.verify;
760
+ if (typeof verify !== "function") throw new TypeError("Local MCP token auth requires a verify function.");
761
+ const deny = () => ({
762
+ ok: false,
763
+ response: new Response(null, {
764
+ status: 401,
765
+ headers: {
766
+ "WWW-Authenticate": "Bearer error=\"invalid_token\"",
767
+ "Cache-Control": "no-store"
768
+ }
769
+ })
770
+ });
771
+ return Object.freeze({
772
+ metadataUrl: "",
773
+ sessionFallback: true,
774
+ metadataResponse: () => new Response(null, {
775
+ status: 404,
776
+ headers: { "Cache-Control": "no-store" }
777
+ }),
778
+ async authenticate(request) {
779
+ const match = LOCAL_BEARER_PATTERN.exec(request.headers.get("authorization") ?? "");
780
+ if (!match) return deny();
781
+ let principal;
782
+ try {
783
+ principal = await verify(match[1]);
784
+ } catch {
785
+ return deny();
786
+ }
787
+ const fields = readOwnFields(principal, [
788
+ "id",
789
+ "tenantId",
790
+ "kind",
791
+ "scopes"
792
+ ]);
793
+ const scopes = fields ? ownStringArray(fields.scopes) : null;
794
+ if (!fields || !scopes || !validString(fields.id) || !validString(fields.tenantId) || fields.kind !== void 0 && !validString(fields.kind) || !scopes.every((scope) => scopePattern.test(scope))) return deny();
795
+ return {
796
+ ok: true,
797
+ principal: {
798
+ id: fields.id,
799
+ tenantId: fields.tenantId,
800
+ kind: fields.kind,
801
+ scopes,
802
+ tenantBinding: "direct"
803
+ }
804
+ };
805
+ }
806
+ });
807
+ }
720
808
  var HOSTED_MCP_AUTH_ENV = Object.freeze({
721
809
  resource: "SMRT_MCP_RESOURCE",
722
810
  issuer: "SMRT_MCP_ISSUER",
@@ -725,9 +813,16 @@ var HOSTED_MCP_AUTH_ENV = Object.freeze({
725
813
  });
726
814
  function createHostedMcpResourceAuth(options) {
727
815
  let cached;
816
+ let local;
817
+ const runtime = options.runtime;
728
818
  return () => {
729
819
  const profile = typeof options.profile === "function" ? options.profile() : options.profile;
730
- if (profile === "local") return null;
820
+ if (profile === "local") {
821
+ const verify = runtime?.verifyLocalMcpToken;
822
+ if (typeof verify !== "function") return null;
823
+ local ??= createLocalMcpTokenAuth({ verify: (token) => verify.call(runtime, token) });
824
+ return local;
825
+ }
731
826
  if (cached) return cached;
732
827
  const env = options.env ?? globalThis.process?.env ?? {};
733
828
  const required = (name) => {
@@ -735,7 +830,9 @@ function createHostedMcpResourceAuth(options) {
735
830
  if (!value) throw new Error(`Hosted MCP requires ${name}.`);
736
831
  return value;
737
832
  };
738
- if (!options.resolvePrincipal) throw new Error("Hosted MCP requires an application-owned resolvePrincipal binding.");
833
+ const runtimeResolver = runtime?.resolveMcpPrincipal;
834
+ const resolvePrincipal = options.resolvePrincipal ?? (typeof runtimeResolver === "function" ? (identity) => runtimeResolver.call(runtime, identity) : void 0);
835
+ if (!resolvePrincipal) throw new Error("Hosted MCP requires an application-owned resolvePrincipal binding or a runtime with resolveMcpPrincipal.");
739
836
  cached = createMcpResourceAuth({
740
837
  profile,
741
838
  resource: required(HOSTED_MCP_AUTH_ENV.resource),
@@ -745,7 +842,7 @@ function createHostedMcpResourceAuth(options) {
745
842
  algorithms: options.algorithms ?? ["RS256"],
746
843
  tokenType: options.tokenType,
747
844
  requireTenant: options.requireTenant,
748
- resolvePrincipal: options.resolvePrincipal
845
+ resolvePrincipal
749
846
  });
750
847
  return cached;
751
848
  };
@@ -819,21 +916,29 @@ function createMcpResourceAuth(options) {
819
916
  const granted = payload.scope === "" ? [] : payload.scope.split(" ");
820
917
  if (granted.some((scope) => !scopePattern.test(scope))) return deny("invalid_token");
821
918
  if (scopes.some((scope) => !granted.includes(scope))) return deny("insufficient_scope");
822
- const mapping = await resolvePrincipal(Object.freeze({
919
+ const fields = readOwnFields(await resolvePrincipal(Object.freeze({
823
920
  issuer,
824
921
  subject: payload.sub,
825
922
  scopes: Object.freeze([...granted]),
826
923
  claims: Object.freeze(payload)
827
- }));
828
- if (!mapping || !validString(mapping.id) || requireTenant && !validString(mapping.tenantId) || mapping.tenantId !== void 0 && !validString(mapping.tenantId) || mapping.kind !== void 0 && !validString(mapping.kind) || mapping.roles !== void 0 && (!Array.isArray(mapping.roles) || !mapping.roles.every(validString))) return deny("invalid_token");
924
+ })), [
925
+ "id",
926
+ "tenantId",
927
+ "kind",
928
+ "roles",
929
+ "tenantBinding"
930
+ ]);
931
+ const roles = fields?.roles === void 0 ? void 0 : ownStringArray(fields.roles);
932
+ if (!fields || roles === null || !validString(fields.id) || requireTenant && !validString(fields.tenantId) || fields.tenantId !== void 0 && !validString(fields.tenantId) || fields.kind !== void 0 && !validString(fields.kind) || roles !== void 0 && !roles.every(validString) || fields.tenantBinding !== void 0 && (typeof fields.tenantBinding !== "string" || !TENANT_BINDINGS.includes(fields.tenantBinding))) return deny("invalid_token");
829
933
  return {
830
934
  ok: true,
831
935
  principal: {
832
- id: mapping.id,
833
- tenantId: mapping.tenantId,
834
- kind: mapping.kind,
835
- roles: mapping.roles ? [...mapping.roles] : void 0,
836
- scopes: [...granted]
936
+ id: fields.id,
937
+ tenantId: fields.tenantId,
938
+ kind: fields.kind,
939
+ roles,
940
+ scopes: [...granted],
941
+ tenantBinding: fields.tenantBinding ?? "direct-or-inherited"
837
942
  }
838
943
  };
839
944
  } catch {
@@ -843,6 +948,6 @@ function createMcpResourceAuth(options) {
843
948
  };
844
949
  }
845
950
  //#endregion
846
- export { HOSTED_MCP_AUTH_ENV, createHostedMcpResourceAuth, createMcpResourceAuth };
951
+ export { HOSTED_MCP_AUTH_ENV, createHostedMcpResourceAuth, createLocalMcpTokenAuth, createMcpResourceAuth };
847
952
 
848
953
  //# sourceMappingURL=auth.js.map