@toolgrain/mcp 0.1.3 → 0.2.0-rc.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @toolgrain/mcp
2
2
 
3
+ ## 0.2.0-rc.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9c5bffd: Declare resource OAuth scopes with `scopesSupported` on auth providers, including direct Clerk and JWT configuration. Validate and copy scope catalogs and emit them as `scopes_supported` in protected-resource discovery without imposing a required-scope policy.
8
+
9
+ Export Clerk's built-in OAuth scope catalog and named scope types, preserving editor completion and support for instance-defined custom scopes.
10
+
11
+ Add `AuthError("insufficient_scope", { requiredScopes, description })` for verified-token scope denials. HTTP adapters return a 403 Bearer challenge with optional required scopes and explicit public guidance, preserving private causes and existing authentication behavior.
12
+
13
+ ### Patch Changes
14
+
15
+ - @toolgrain/protocol@0.2.0-rc.0
16
+
3
17
  ## 0.1.3
4
18
 
5
19
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"authorization.d.ts","sourceRoot":"","sources":["../../src/auth/authorization.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,SAAS,EACV,MAAM,YAAY,CAAC;AASpB,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,YAAY;IAsBvD,QAAQ;IACF,SAAS,QACN,MAAM,YACH,MAAM,UACR,WAAW,UACX,MAAM,IAAI,GACjB,OAAO,CAAC,SAAS,CAAC;IA0BrB,OAAO,SAAS,SAAS,GAAG,YAAY,GAAG,SAAS;EASvD"}
1
+ {"version":3,"file":"authorization.d.ts","sourceRoot":"","sources":["../../src/auth/authorization.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,SAAS,EACV,MAAM,YAAY,CAAC;AASpB,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,YAAY;IAyBvD,QAAQ;IACF,SAAS,QACN,MAAM,YACH,MAAM,UACR,WAAW,UACX,MAAM,IAAI,GACjB,OAAO,CAAC,SAAS,CAAC;IA0BrB,OAAO,SAAS,SAAS,GAAG,YAAY,GAAG,SAAS;EASvD"}
@@ -1,6 +1,7 @@
1
1
  import { abortable } from "./abort.js";
2
2
  import { identitySnapshot } from "./context.js";
3
3
  import { AuthError } from "./errors.js";
4
+ import { validateAuthScopes } from "./scopes.js";
4
5
  import { authUrl } from "./url.js";
5
6
  export function createAuthorization(provider) {
6
7
  const scopes = new WeakMap();
@@ -12,6 +13,9 @@ export function createAuthorization(provider) {
12
13
  authUrl(issuer);
13
14
  return issuer;
14
15
  }),
16
+ ...(provider.scopesSupported === undefined
17
+ ? {}
18
+ : { scopesSupported: validateAuthScopes(provider.scopesSupported) }),
15
19
  };
16
20
  if (provider &&
17
21
  (typeof provider.type !== "symbol" ||
@@ -1 +1 @@
1
- {"version":3,"file":"authorization.js","sourceRoot":"","sources":["../../src/auth/authorization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAOxC,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAQnC,MAAM,UAAU,mBAAmB,CAAC,QAAuB;IACzD,MAAM,MAAM,GAAG,IAAI,OAAO,EAAoB,CAAC;IAC/C,MAAM,QAAQ,GAA6B,QAAQ,IAAI;QACrD,GAAG,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS;YACjC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7C,oBAAoB,EAAE,QAAQ,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;YACjE,OAAO,CAAC,MAAM,CAAC,CAAC;YAChB,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC;KACH,CAAC;IACF,IACE,QAAQ;QACR,CAAC,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ;YAChC,CAAC,QAAQ,EAAE,oBAAoB,CAAC,MAAM,CAAC;QAEzC,MAAM,IAAI,SAAS,CACjB,4EAA4E,CAC7E,CAAC;IACJ,MAAM,MAAM,GAAG,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,QAAQ,EAAE,IAAI,CAAC;IAC5B,OAAO;QACL,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,IAAI,eAAe,CAAC,QAAQ,CAAC;QACrD,KAAK,CAAC,SAAS,CACb,KAAa,EACb,QAAgB,EAChB,MAAmB,EACnB,MAAkB;YAElB,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI;gBAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;YACrE,MAAM,CAAC,cAAc,EAAE,CAAC;YACxB,MAAM,QAAQ,GAAG,QAAQ,EAAE,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;YACzD,MAAM,QAAQ,GAAG;gBACf,IAAI;gBACJ,KAAK,EAAE,gBAAgB,CACrB,MAAM,SAAS,CACb,MAAM,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAC7C,MAAM,CACP,CACF;aACF,CAAC;YACF,MAAM,CAAC,cAAc,EAAE,CAAC;YACxB,MAAM,KAAK,GAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;YACpE,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;gBAC1B,MAAM;gBACN,OAAO;oBACL,IAAI,KAAK,CAAC,QAAQ;wBAAE,OAAO;oBAC3B,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC;oBACtB,MAAM,EAAE,CAAC;gBACX,CAAC;aACF,CAAC,CAAC;YACH,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACzB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,CAAC,KAAiB;YACvB,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK;gBAAE,OAAO;YAChC,MAAM,KAAK,GAAG,KAAK,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACzC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ;gBAC5C,MAAM,IAAI,SAAS,CAAC,eAAe,CAAC,CAAC;YACvC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC;YACtB,OAAO,KAAK,CAAC,QAAQ,CAAC;QACxB,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"authorization.js","sourceRoot":"","sources":["../../src/auth/authorization.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /** Public authorization failure categories mapped by HTTP adapters to 400, 401, 403, or 503. */
2
- export type AuthErrorCode = "invalid_request" | "invalid_token" | "access_denied" | "temporarily_unavailable";
2
+ export type AuthErrorCode = "invalid_request" | "invalid_token" | "access_denied" | "insufficient_scope" | "temporarily_unavailable";
3
3
  declare const brand: unique symbol;
4
4
  /** Only an explicitly public description is exposed; provider causes stay private. */
5
5
  export declare class AuthError extends Error {
@@ -8,17 +8,22 @@ export declare class AuthError extends Error {
8
8
  readonly [brand] = true;
9
9
  /** Explicitly client-visible guidance; omitted descriptions keep HTTP responses generic. */
10
10
  readonly description?: string | undefined;
11
+ /** Copied, frozen required scopes for an `insufficient_scope` Bearer challenge; never inferred from supported scopes. */
12
+ readonly requiredScopes?: readonly string[] | undefined;
11
13
  /**
12
14
  * Create an authorization failure with optional public guidance and a private cause.
13
15
  *
14
16
  * @param code - Authorization failure category used by HTTP adapters.
15
- * @param options - Optional public description and private standard `Error` options.
17
+ * @param options - Optional public description/required scopes and private standard `Error` options.
18
+ * @throws `TypeError` for invalid required scopes or required scopes on another error code.
16
19
  */
17
20
  constructor(
18
21
  /** Failure category; determines the default message and HTTP status. */
19
22
  code: AuthErrorCode, options?: ErrorOptions & {
20
23
  /** Public message included in HTTP error bodies and challenges; never include secrets. */
21
24
  description?: string;
25
+ /** Public required scope tokens, only for `insufficient_scope`; an empty array omits the challenge's scope field. */
26
+ requiredScopes?: readonly string[];
22
27
  });
23
28
  }
24
29
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/auth/errors.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB,eAAe,GACf,eAAe,GACf,yBAAyB,CAAC;AAE9B,QAAA,MAAM,KAAK,eAA4C,CAAC;AAExD,sFAAsF;AACtF,qBAAa,SAAU,SAAQ,KAAK;IAYhC,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,aAAa;IAZ9B,QAAQ,CAAC,CAAC,KAAK,CAAC,QAAQ;IACxB,4FAA4F;IAC5F,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE1C;;;;;OAKG;IACH;IACE,wEAAwE;IAC/D,IAAI,EAAE,aAAa,EAC5B,OAAO,CAAC,EAAE,YAAY,GAAG;QACvB,0FAA0F;QAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,EAcF;CACF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAE9D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/auth/errors.ts"],"names":[],"mappings":"AAEA,gGAAgG;AAChG,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB,eAAe,GACf,eAAe,GACf,oBAAoB,GACpB,yBAAyB,CAAC;AAE9B,QAAA,MAAM,KAAK,eAA4C,CAAC;AAExD,sFAAsF;AACtF,qBAAa,SAAU,SAAQ,KAAK;IAehC,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,aAAa;IAf9B,QAAQ,CAAC,CAAC,KAAK,CAAC,QAAQ;IACxB,4FAA4F;IAC5F,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,yHAAyH;IACzH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAExD;;;;;;OAMG;IACH;IACE,wEAAwE;IAC/D,IAAI,EAAE,aAAa,EAC5B,OAAO,CAAC,EAAE,YAAY,GAAG;QACvB,0FAA0F;QAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,qHAAqH;QACrH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;KACpC,EAsBF;CACF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAE9D"}
@@ -1,3 +1,4 @@
1
+ import { validateAuthScopes } from "./scopes.js";
1
2
  const brand = Symbol.for("@toolgrain/mcp/AuthError/v1");
2
3
  /** Only an explicitly public description is exposed; provider causes stay private. */
3
4
  export class AuthError extends Error {
@@ -5,11 +6,14 @@ export class AuthError extends Error {
5
6
  [brand] = true;
6
7
  /** Explicitly client-visible guidance; omitted descriptions keep HTTP responses generic. */
7
8
  description;
9
+ /** Copied, frozen required scopes for an `insufficient_scope` Bearer challenge; never inferred from supported scopes. */
10
+ requiredScopes;
8
11
  /**
9
12
  * Create an authorization failure with optional public guidance and a private cause.
10
13
  *
11
14
  * @param code - Authorization failure category used by HTTP adapters.
12
- * @param options - Optional public description and private standard `Error` options.
15
+ * @param options - Optional public description/required scopes and private standard `Error` options.
16
+ * @throws `TypeError` for invalid required scopes or required scopes on another error code.
13
17
  */
14
18
  constructor(
15
19
  /** Failure category; determines the default message and HTTP status. */
@@ -19,11 +23,17 @@ export class AuthError extends Error {
19
23
  invalid_request: "Invalid authorization request",
20
24
  invalid_token: "Invalid access token",
21
25
  access_denied: "Access denied",
26
+ insufficient_scope: "Access token has insufficient scope",
22
27
  temporarily_unavailable: "Authorization service unavailable",
23
28
  }[code], options);
24
29
  this.code = code;
25
30
  this.name = "AuthError";
26
31
  this.description = options?.description;
32
+ if (options?.requiredScopes !== undefined) {
33
+ if (code !== "insufficient_scope")
34
+ throw new TypeError("Required scopes apply only to insufficient_scope errors");
35
+ this.requiredScopes = validateAuthScopes(options.requiredScopes);
36
+ }
27
37
  }
28
38
  }
29
39
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/auth/errors.ts"],"names":[],"mappings":"AAOA,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;AAExD,sFAAsF;AACtF,MAAM,OAAO,SAAU,SAAQ,KAAK;IAavB,IAAI;IAZN,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IACxB,4FAA4F;IACnF,WAAW,CAAsB;IAE1C;;;;;OAKG;IACH;IACE,wEAAwE;IAC/D,IAAmB,EAC5B,OAGC;QAED,KAAK,CACH,OAAO,EAAE,WAAW;YAClB;gBACE,eAAe,EAAE,+BAA+B;gBAChD,aAAa,EAAE,sBAAsB;gBACrC,aAAa,EAAE,eAAe;gBAC9B,uBAAuB,EAAE,mCAAmC;aAC7D,CAAC,IAAI,CAAC,EACT,OAAO,CACR,CAAC;oBAfO,IAAI;QAgBb,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,WAAW,GAAG,OAAO,EAAE,WAAW,CAAC;IAC1C,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO,KAAK,YAAY,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC;AACtE,CAAC"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/auth/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAUjD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;AAExD,sFAAsF;AACtF,MAAM,OAAO,SAAU,SAAQ,KAAK;IAgBvB,IAAI;IAfN,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IACxB,4FAA4F;IACnF,WAAW,CAAsB;IAC1C,yHAAyH;IAChH,cAAc,CAAiC;IAExD;;;;;;OAMG;IACH;IACE,wEAAwE;IAC/D,IAAmB,EAC5B,OAKC;QAED,KAAK,CACH,OAAO,EAAE,WAAW;YAClB;gBACE,eAAe,EAAE,+BAA+B;gBAChD,aAAa,EAAE,sBAAsB;gBACrC,aAAa,EAAE,eAAe;gBAC9B,kBAAkB,EAAE,qCAAqC;gBACzD,uBAAuB,EAAE,mCAAmC;aAC7D,CAAC,IAAI,CAAC,EACT,OAAO,CACR,CAAC;oBAlBO,IAAI;QAmBb,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,WAAW,GAAG,OAAO,EAAE,WAAW,CAAC;QACxC,IAAI,OAAO,EAAE,cAAc,KAAK,SAAS,EAAE,CAAC;YAC1C,IAAI,IAAI,KAAK,oBAAoB;gBAC/B,MAAM,IAAI,SAAS,CACjB,yDAAyD,CAC1D,CAAC;YACJ,IAAI,CAAC,cAAc,GAAG,kBAAkB,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO,KAAK,YAAY,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC;AACtE,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Validate OAuth scope tokens and return a frozen copy, preserving case and order.
3
+ * An empty array explicitly declares no scopes; omission is handled by the caller.
4
+ *
5
+ * @param scopes - Unique, nonempty ASCII scope tokens from RFC 6749 section 3.3.
6
+ * @returns A readonly snapshot suitable for resource metadata or scope challenges.
7
+ * @throws `TypeError` for non-arrays, duplicates, whitespace, quotes, backslashes, or non-ASCII tokens.
8
+ */
9
+ export declare function validateAuthScopes(scopes: readonly string[]): readonly string[];
10
+ //# sourceMappingURL=scopes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../../src/auth/scopes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,SAAS,MAAM,EAAE,CAmBnB"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Validate OAuth scope tokens and return a frozen copy, preserving case and order.
3
+ * An empty array explicitly declares no scopes; omission is handled by the caller.
4
+ *
5
+ * @param scopes - Unique, nonempty ASCII scope tokens from RFC 6749 section 3.3.
6
+ * @returns A readonly snapshot suitable for resource metadata or scope challenges.
7
+ * @throws `TypeError` for non-arrays, duplicates, whitespace, quotes, backslashes, or non-ASCII tokens.
8
+ */
9
+ export function validateAuthScopes(scopes) {
10
+ const copy = [];
11
+ const seen = new Set();
12
+ if (!Array.isArray(scopes))
13
+ throw new TypeError("OAuth scopes must be an array of scope tokens");
14
+ for (const scope of scopes) {
15
+ if (typeof scope !== "string" ||
16
+ !scope.length ||
17
+ /[^\x21\x23-\x5b\x5d-\x7e]/.test(scope) ||
18
+ seen.has(scope))
19
+ throw new TypeError("OAuth scopes must be unique, nonempty RFC 6749 scope tokens");
20
+ seen.add(scope);
21
+ copy.push(scope);
22
+ }
23
+ return Object.freeze(copy);
24
+ }
25
+ //# sourceMappingURL=scopes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scopes.js","sourceRoot":"","sources":["../../src/auth/scopes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAyB;IAEzB,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QACxB,MAAM,IAAI,SAAS,CAAC,+CAA+C,CAAC,CAAC;IACvE,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IACE,OAAO,KAAK,KAAK,QAAQ;YACzB,CAAC,KAAK,CAAC,MAAM;YACb,2BAA2B,CAAC,IAAI,CAAC,KAAK,CAAC;YACvC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;YAEf,MAAM,IAAI,SAAS,CACjB,6DAA6D,CAC9D,CAAC;QACJ,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC"}
@@ -6,6 +6,8 @@ export interface AuthProvider<Identity extends object = object> {
6
6
  readonly resource?: string;
7
7
  /** Nonempty list of trusted authorization-server URLs advertised in public metadata. */
8
8
  readonly authorizationServers: readonly string[];
9
+ /** Optional resource scope catalog for discovery, copied at server creation; not a required-scope policy. */
10
+ readonly scopesSupported?: readonly string[];
9
11
  /**
10
12
  * Verify token authenticity, validity, and resource audience before returning plain identity data.
11
13
  * The server copies and deeply freezes the identity before exposing it to handlers.
@@ -30,6 +32,8 @@ export interface AuthMetadata {
30
32
  readonly resource?: string;
31
33
  /** Trusted authorization-server URLs advertised by protected-resource metadata. */
32
34
  readonly authorizationServers: readonly string[];
35
+ /** Resource scopes advertised as `scopes_supported`; absent means unspecified, empty means none. */
36
+ readonly scopesSupported?: readonly string[];
33
37
  }
34
38
  /** An opaque, single-use authorization for one server invocation. Always dispose it. */
35
39
  export interface AuthScope {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,WAAW,YAAY,CAAC,QAAQ,SAAS,MAAM,GAAG,MAAM;IAC5D,iGAAiG;IACjG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8FAA8F;IAC9F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD;;;;;;;;OAQG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5E;AAED,mFAAmF;AACnF,MAAM,WAAW,iBAAiB;IAChC,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,4FAA4F;IAC5F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;CAClD;AAED,wFAAwF;AACxF,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,uFAAuF;IACvF,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,0FAA0F;AAC1F,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,CAAC,MAAM,IAAI,CAAC,EAAE,GAC3D,SAAS,YAAY,CAAC,IAAI,CAAC,EAAE,GAC7B,CAAC,SAAS,MAAM,GACd;IAAE,QAAQ,EAAE,GAAG,IAAI,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;CAAE,GACnD,CAAC,CAAC;AAER,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,WAAW,YAAY,CAAC,QAAQ,SAAS,MAAM,GAAG,MAAM;IAC5D,iGAAiG;IACjG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8FAA8F;IAC9F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD,6GAA6G;IAC7G,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C;;;;;;;;OAQG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5E;AAED,mFAAmF;AACnF,MAAM,WAAW,iBAAiB;IAChC,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,4FAA4F;IAC5F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD,oGAAoG;IACpG,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AAED,wFAAwF;AACxF,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,uFAAuF;IACvF,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,0FAA0F;AAC1F,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,CAAC,MAAM,IAAI,CAAC,EAAE,GAC3D,SAAS,YAAY,CAAC,IAAI,CAAC,EAAE,GAC7B,CAAC,SAAS,MAAM,GACd;IAAE,QAAQ,EAAE,GAAG,IAAI,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;CAAE,GACnD,CAAC,CAAC;AAER,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB"}
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { validateTokenAudience } from "./auth/audience.js";
2
2
  export { createAuthAccessor } from "./auth/context.js";
3
3
  export type { AuthErrorCode } from "./auth/errors.js";
4
4
  export { AuthError, isAuthError } from "./auth/errors.js";
5
+ export { validateAuthScopes } from "./auth/scopes.js";
5
6
  export type { AuthMetadata, AuthProvider, AuthReadonly, AuthScope, AuthVerifyOptions, } from "./auth/types.js";
6
7
  export { authUrl as validateAuthUrl } from "./auth/url.js";
7
8
  export type { ContextProvider } from "./context/definition.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC1D,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,eAAe,CAAC;AAC3D,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EACV,UAAU,EACV,WAAW,EACX,kBAAkB,EAClB,WAAW,GACZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,YAAY,EAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EACV,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,kBAAkB,GACnB,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,eAAe,CAAC;AAC3D,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EACV,UAAU,EACV,WAAW,EACX,kBAAkB,EAClB,WAAW,GACZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,YAAY,EAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EACV,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,kBAAkB,GACnB,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { validateTokenAudience } from "./auth/audience.js";
2
2
  export { createAuthAccessor } from "./auth/context.js";
3
3
  export { AuthError, isAuthError } from "./auth/errors.js";
4
+ export { validateAuthScopes } from "./auth/scopes.js";
4
5
  export { authUrl as validateAuthUrl } from "./auth/url.js";
5
6
  export { createContext } from "./context/definition.js";
6
7
  export { createServer } from "./server.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAQ1D,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,eAAe,CAAC;AAE3D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAGxD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAO3C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAQhD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAQtD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,eAAe,CAAC;AAE3D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAGxD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAO3C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAQhD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolgrain/mcp",
3
- "version": "0.1.3",
3
+ "version": "0.2.0-rc.0",
4
4
  "description": "Define reusable, typed MCP tools and servers with Zod validation.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  "publishConfig": {
24
24
  "access": "public",
25
25
  "registry": "https://registry.npmjs.org/",
26
- "tag": "latest"
26
+ "tag": "rc"
27
27
  },
28
28
  "type": "module",
29
29
  "exports": {
@@ -59,7 +59,7 @@
59
59
  "@toolgrain/repo-tools": "0.0.0"
60
60
  },
61
61
  "dependencies": {
62
- "@toolgrain/protocol": "0.1.3"
62
+ "@toolgrain/protocol": "0.2.0-rc.0"
63
63
  },
64
64
  "engines": {
65
65
  "node": "^24.20.0",
@@ -1,6 +1,7 @@
1
1
  import { abortable } from "./abort.js";
2
2
  import { identitySnapshot } from "./context.js";
3
3
  import { AuthError } from "./errors.js";
4
+ import { validateAuthScopes } from "./scopes.js";
4
5
  import type {
5
6
  AuthIdentity,
6
7
  AuthMetadata,
@@ -25,6 +26,9 @@ export function createAuthorization(provider?: AuthProvider) {
25
26
  authUrl(issuer);
26
27
  return issuer;
27
28
  }),
29
+ ...(provider.scopesSupported === undefined
30
+ ? {}
31
+ : { scopesSupported: validateAuthScopes(provider.scopesSupported) }),
28
32
  };
29
33
  if (
30
34
  provider &&
@@ -1,8 +1,11 @@
1
+ import { validateAuthScopes } from "./scopes.js";
2
+
1
3
  /** Public authorization failure categories mapped by HTTP adapters to 400, 401, 403, or 503. */
2
4
  export type AuthErrorCode =
3
5
  | "invalid_request"
4
6
  | "invalid_token"
5
7
  | "access_denied"
8
+ | "insufficient_scope"
6
9
  | "temporarily_unavailable";
7
10
 
8
11
  const brand = Symbol.for("@toolgrain/mcp/AuthError/v1");
@@ -12,12 +15,15 @@ export class AuthError extends Error {
12
15
  readonly [brand] = true;
13
16
  /** Explicitly client-visible guidance; omitted descriptions keep HTTP responses generic. */
14
17
  readonly description?: string | undefined;
18
+ /** Copied, frozen required scopes for an `insufficient_scope` Bearer challenge; never inferred from supported scopes. */
19
+ readonly requiredScopes?: readonly string[] | undefined;
15
20
 
16
21
  /**
17
22
  * Create an authorization failure with optional public guidance and a private cause.
18
23
  *
19
24
  * @param code - Authorization failure category used by HTTP adapters.
20
- * @param options - Optional public description and private standard `Error` options.
25
+ * @param options - Optional public description/required scopes and private standard `Error` options.
26
+ * @throws `TypeError` for invalid required scopes or required scopes on another error code.
21
27
  */
22
28
  constructor(
23
29
  /** Failure category; determines the default message and HTTP status. */
@@ -25,6 +31,8 @@ export class AuthError extends Error {
25
31
  options?: ErrorOptions & {
26
32
  /** Public message included in HTTP error bodies and challenges; never include secrets. */
27
33
  description?: string;
34
+ /** Public required scope tokens, only for `insufficient_scope`; an empty array omits the challenge's scope field. */
35
+ requiredScopes?: readonly string[];
28
36
  },
29
37
  ) {
30
38
  super(
@@ -33,12 +41,20 @@ export class AuthError extends Error {
33
41
  invalid_request: "Invalid authorization request",
34
42
  invalid_token: "Invalid access token",
35
43
  access_denied: "Access denied",
44
+ insufficient_scope: "Access token has insufficient scope",
36
45
  temporarily_unavailable: "Authorization service unavailable",
37
46
  }[code],
38
47
  options,
39
48
  );
40
49
  this.name = "AuthError";
41
50
  this.description = options?.description;
51
+ if (options?.requiredScopes !== undefined) {
52
+ if (code !== "insufficient_scope")
53
+ throw new TypeError(
54
+ "Required scopes apply only to insufficient_scope errors",
55
+ );
56
+ this.requiredScopes = validateAuthScopes(options.requiredScopes);
57
+ }
42
58
  }
43
59
  }
44
60
 
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Validate OAuth scope tokens and return a frozen copy, preserving case and order.
3
+ * An empty array explicitly declares no scopes; omission is handled by the caller.
4
+ *
5
+ * @param scopes - Unique, nonempty ASCII scope tokens from RFC 6749 section 3.3.
6
+ * @returns A readonly snapshot suitable for resource metadata or scope challenges.
7
+ * @throws `TypeError` for non-arrays, duplicates, whitespace, quotes, backslashes, or non-ASCII tokens.
8
+ */
9
+ export function validateAuthScopes(
10
+ scopes: readonly string[],
11
+ ): readonly string[] {
12
+ const copy: string[] = [];
13
+ const seen = new Set<string>();
14
+ if (!Array.isArray(scopes))
15
+ throw new TypeError("OAuth scopes must be an array of scope tokens");
16
+ for (const scope of scopes) {
17
+ if (
18
+ typeof scope !== "string" ||
19
+ !scope.length ||
20
+ /[^\x21\x23-\x5b\x5d-\x7e]/.test(scope) ||
21
+ seen.has(scope)
22
+ )
23
+ throw new TypeError(
24
+ "OAuth scopes must be unique, nonempty RFC 6749 scope tokens",
25
+ );
26
+ seen.add(scope);
27
+ copy.push(scope);
28
+ }
29
+ return Object.freeze(copy);
30
+ }
package/src/auth/types.ts CHANGED
@@ -6,6 +6,8 @@ export interface AuthProvider<Identity extends object = object> {
6
6
  readonly resource?: string;
7
7
  /** Nonempty list of trusted authorization-server URLs advertised in public metadata. */
8
8
  readonly authorizationServers: readonly string[];
9
+ /** Optional resource scope catalog for discovery, copied at server creation; not a required-scope policy. */
10
+ readonly scopesSupported?: readonly string[];
9
11
  /**
10
12
  * Verify token authenticity, validity, and resource audience before returning plain identity data.
11
13
  * The server copies and deeply freezes the identity before exposing it to handlers.
@@ -32,6 +34,8 @@ export interface AuthMetadata {
32
34
  readonly resource?: string;
33
35
  /** Trusted authorization-server URLs advertised by protected-resource metadata. */
34
36
  readonly authorizationServers: readonly string[];
37
+ /** Resource scopes advertised as `scopes_supported`; absent means unspecified, empty means none. */
38
+ readonly scopesSupported?: readonly string[];
35
39
  }
36
40
 
37
41
  /** An opaque, single-use authorization for one server invocation. Always dispose it. */
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export { validateTokenAudience } from "./auth/audience.js";
2
2
  export { createAuthAccessor } from "./auth/context.js";
3
3
  export type { AuthErrorCode } from "./auth/errors.js";
4
4
  export { AuthError, isAuthError } from "./auth/errors.js";
5
+ export { validateAuthScopes } from "./auth/scopes.js";
5
6
  export type {
6
7
  AuthMetadata,
7
8
  AuthProvider,