refactor: move the schema source modules into @modelcontextprotocol/core (#2477)

This commit is contained in:
Felix Weinberger 2026-07-13 17:19:58 +01:00 • committed by GitHub
commit 8e1d2e92b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 3597 additions and 2914 deletions

View file

@ -133,6 +133,7 @@
"test:watch": "vitest"
},
"dependencies": {
"@modelcontextprotocol/core": "workspace:*",
"cross-spawn": "catalog:runtimeClientOnly",
"eventsource": "catalog:runtimeClientOnly",
"eventsource-parser": "catalog:runtimeClientOnly",

View file

@ -1,11 +1,12 @@
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { beforeAll, describe, expect, test } from 'vitest';
import { ensureBuilt } from '../helpers/ensureBuilt';
const pkgDir = join(dirname(fileURLToPath(import.meta.url)), '../..');
const distDir = join(pkgDir, 'dist');
const requireDist = createRequire(join(pkgDir, 'package.json'));
@ -37,11 +38,9 @@ function rootExportBlockOf(content: string): string {
}
describe('@modelcontextprotocol/client root entry is browser-safe', () => {
beforeAll(() => {
if (!existsSync(join(distDir, 'index.mjs')) || !existsSync(join(distDir, 'stdio.mjs'))) {
execFileSync('pnpm', ['build'], { cwd: pkgDir, stdio: 'inherit' });
}
}, 60_000);
beforeAll(async () => {
await ensureBuilt(pkgDir);
}, 180_000);
test('dist/index.mjs contains no process-spawning runtime imports', () => {
const entry = join(distDir, 'index.mjs');

View file

@ -0,0 +1,168 @@
/**
* Schema-module boundary pins: the built client resolves its schemas from
* @modelcontextprotocol/core instead of carrying its own copies.
*
* The client bundle keeps `@modelcontextprotocol/core/internal` as an external
* runtime import (see tsdown.config.ts), so two things can silently go wrong:
*
* 1. Skew — the client dist imports a name that core's built entries no longer
* export (e.g. a schema added to core-internal's shims without adding it to
* core, or a rename that only landed on one side). That fails at consumer
* runtime, not at build time.
* 2. Re-inlining — a config change (dropping the `external` entry, a paths
* alias resolving too early) makes the bundler inline the schema sources
* again, duplicating hundreds of Zod schemas into every sibling package.
*
* Both directions are asserted against the real build outputs. Only the ESM
* chunks are parsed; the CJS build is produced from the same module graph, so
* skew shows up identically in both.
*/
import { readdirSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { beforeAll, describe, expect, test } from 'vitest';
import { ensureBuilt } from '../helpers/ensureBuilt';
const clientPkgDir = join(dirname(fileURLToPath(import.meta.url)), '../..');
const clientDistDir = join(clientPkgDir, 'dist');
const corePkgDir = join(clientPkgDir, '..', 'core');
const coreDistDir = join(corePkgDir, 'dist');
/** Built core entry file per import specifier the client may use. */
const CORE_ENTRIES: Record<string, string> = {
'@modelcontextprotocol/core': 'index.mjs',
'@modelcontextprotocol/core/internal': 'internal.mjs'
};
/**
* Sentinel schemas that exist ONLY in core's source modules. The frozen
* wire-era modules (bundled into the client on purpose) define their own
* copies of many spec schema names, but not these — so a `const` definition
* of any of them inside the client dist can only mean the neutral schema
* modules got re-inlined. The first two are names the client actually uses,
* so they must also show up as imports; SafeUrlSchema is unused by the client
* (legitimately tree-shaken away) and is pinned as never-defined only.
*/
const IMPORTED_SENTINELS = ['OAuthMetadataSchema', 'JSONRPCMessageSchema'];
const NEVER_DEFINED_SENTINELS = [...IMPORTED_SENTINELS, 'SafeUrlSchema'];
function clientChunks(): string[] {
return readdirSync(clientDistDir, { recursive: true })
.map(String)
.filter(f => f.endsWith('.mjs'));
}
/** Exported names of a built core entry (its trailing `export { a, b as c };` blocks). */
function coreExportedNames(entryFile: string): Set<string> {
const src = readFileSync(join(coreDistDir, entryFile), 'utf8');
const blocks = [...src.matchAll(/export \{([\s\S]*?)\}/g)];
expect(blocks.length, `no export block found in core dist/${entryFile}`).toBeGreaterThan(0);
const names = new Set<string>();
for (const block of blocks) {
for (const entry of block[1]!.split(',')) {
const name = entry
.trim()
.split(/\s+as\s+/)
.pop()
?.trim();
if (name) {
names.add(name);
}
}
}
return names;
}
/** All names a client chunk pulls from a core specifier, keyed by specifier. */
function coreImportsOf(chunkSource: string): Map<string, Set<string>> {
const imports = new Map<string, Set<string>>();
// import { A, B as C } from "@modelcontextprotocol/core[/internal]"
// export { A, B as C } from "@modelcontextprotocol/core[/internal]"
for (const m of chunkSource.matchAll(
/(?:import|export)\s*\{([^}]*)\}\s*from\s*["'](@modelcontextprotocol\/core(?:\/internal)?)["']/g
)) {
const names = imports.get(m[2]!) ?? new Set<string>();
for (const entry of m[1]!.split(',')) {
// In both clause forms the name resolved against core is the one BEFORE `as`.
const name = entry
.trim()
.split(/\s+as\s+/)[0]
?.trim();
if (name) {
names.add(name);
}
}
imports.set(m[2]!, names);
}
return imports;
}
describe('@modelcontextprotocol/client ↔ core schema boundary', () => {
beforeAll(async () => {
await ensureBuilt(corePkgDir);
await ensureBuilt(clientPkgDir);
}, 240_000);
test('every name the client dist imports from core resolves against core’s built exports', () => {
let sawCoreImport = false;
for (const chunk of clientChunks()) {
const source = readFileSync(join(clientDistDir, chunk), 'utf8');
for (const [specifier, names] of coreImportsOf(source)) {
sawCoreImport = true;
const entryFile = CORE_ENTRIES[specifier];
expect(entryFile, `client dist/${chunk} imports unknown core subpath ${specifier}`).toBeDefined();
const exported = coreExportedNames(entryFile!);
const missing = [...names].filter(name => !exported.has(name));
expect(
missing,
`client dist/${chunk} imports names from ${specifier} that core's built ${entryFile} does not export`
).toEqual([]);
}
// Non-named forms (namespace/default/bare imports, `export *`) would dodge the check above.
expect(
source,
`client dist/${chunk} uses a non-named import/re-export of @modelcontextprotocol/core — update this test to cover it`
).not.toMatch(/(?:import|export)\s+(?!\{)[^;]*?from\s*["']@modelcontextprotocol\/core(?:\/internal)?["']/);
// Bare side-effect imports have no `from` clause and would dodge the pattern above.
expect(
source,
`client dist/${chunk} uses a bare side-effect import of @modelcontextprotocol/core — update this test to cover it`
).not.toMatch(/import\s*["']@modelcontextprotocol\/core(?:\/internal)?["']/);
}
expect(sawCoreImport, 'no client chunk imports @modelcontextprotocol/core at all — external wiring changed?').toBe(true);
});
test('the neutral schema bodies stay OUT of the client dist (imported, never re-inlined)', () => {
const sources = clientChunks().map(chunk => ({
chunk,
source: readFileSync(join(clientDistDir, chunk), 'utf8')
}));
const importedNames = new Set<string>();
for (const { source } of sources) {
for (const names of coreImportsOf(source).values()) {
for (const name of names) {
importedNames.add(name);
}
}
}
for (const sentinel of IMPORTED_SENTINELS) {
expect(importedNames.has(sentinel), `${sentinel} is not imported from core by any client chunk`).toBe(true);
}
for (const sentinel of NEVER_DEFINED_SENTINELS) {
// `$N` suffix included: a re-inlined copy colliding with the import gets renamed by the
// bundler. The `[=(]` tail covers both plain bindings and function-form definitions.
const definition = new RegExp(`\\b(?:const|let|var|function)\\s+${sentinel}(?:\\$\\d+)?\\s*[=(]`);
for (const { chunk, source } of sources) {
expect(
source,
`client dist/${chunk} DEFINES ${sentinel} instead of importing it from @modelcontextprotocol/core — the neutral schema modules were re-inlined`
).not.toMatch(definition);
}
}
});
});

View file

@ -0,0 +1,98 @@
import { execFile } from 'node:child_process';
import { existsSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs';
import { basename, join } from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
/**
* Canonical dist sentinels per package. Every caller waits on the same
* complete set: if callers checked their own subsets, a partial dist (from an
* interrupted build) could satisfy one caller's fast path while another still
* rebuilds — and the rebuild's clean step would wipe files under the first
* caller's running tests, which is exactly the race this helper exists to
* prevent.
*/
const DIST_SENTINELS: Record<string, string[]> = {
// Include late-written artifacts (dts pass, CJS validators) so an
// interrupted build never satisfies the fast path.
client: ['index.mjs', 'stdio.mjs', 'validators/ajv.cjs', 'index.d.mts', 'index.d.cts'],
core: ['index.mjs', 'internal.mjs', 'index.d.mts', 'internal.d.mts']
};
/** The build is killed after this long; execFile's kill still runs our finally. */
const BUILD_TIMEOUT_MS = 60_000;
/** How long a waiter polls for another worker's build — outlasts BUILD_TIMEOUT_MS. */
const WAIT_DEADLINE_MS = 90_000;
/** A lock older than this belongs to a dead worker (finally never ran) — steal it. */
const STALE_LOCK_MS = 120_000;
/**
* Build a package's dist on demand, safely across parallel vitest workers.
*
* Vitest runs test files in separate worker processes, so two files that each
* "build if dist is missing" can race on a cold checkout: both see no dist,
* both spawn `pnpm build`, and tsdown's clean step deletes files out from
* under whichever worker is already reading them. This helper makes the build
* single-flight with an atomic mkdir lock: the first worker builds, everyone
* else waits for the sentinel files to appear and the lock to clear, and once
* the dist exists nobody ever rebuilds (so no later clean can wipe it
* mid-read).
*/
export async function ensureBuilt(pkgDir: string): Promise<void> {
const sentinels = DIST_SENTINELS[basename(pkgDir)];
if (!sentinels) {
throw new Error(`No dist sentinels registered for ${pkgDir} — add the package to DIST_SENTINELS`);
}
const lockDir = join(pkgDir, '.dist-build-lock');
const haveAll = () => sentinels.every(s => existsSync(join(pkgDir, 'dist', s)));
const deadline = Date.now() + WAIT_DEADLINE_MS;
for (;;) {
if (haveAll() && !existsSync(lockDir)) return;
try {
mkdirSync(lockDir);
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;
// Another worker holds the lock. If the holder died without its
// finally running (SIGKILL/OOM), the lock never clears — steal it
// once it is older than any live build could be.
try {
if (Date.now() - statSync(lockDir).mtimeMs > STALE_LOCK_MS) {
// renameSync is atomic: exactly one waiter steals; losers throw and re-check.
const graveyard = `${lockDir}.stale-${process.pid}-${Date.now()}`;
renameSync(lockDir, graveyard);
rmSync(graveyard, { recursive: true, force: true });
continue;
}
} catch {
continue; // lock vanished or another waiter stole it — re-check now
}
if (Date.now() > deadline) {
throw new Error(
`Timed out waiting for ${pkgDir}/dist (${sentinels.join(', ')}) ` +
`while another worker held the build lock at ${lockDir}`
);
}
await sleep(250);
continue;
}
try {
if (!haveAll()) {
try {
await execFileAsync('pnpm', ['build'], {
cwd: pkgDir,
timeout: BUILD_TIMEOUT_MS,
maxBuffer: 16 * 1024 * 1024
});
} catch (err) {
const stderr = (err as { stderr?: string }).stderr ?? '';
throw new Error(`pnpm build failed in ${pkgDir}: ${(err as Error).message}\n${stderr.slice(-2000)}`);
}
}
return;
} finally {
rmSync(lockDir, { recursive: true, force: true });
}
}
}

View file

@ -6,6 +6,7 @@
"paths": {
"*": ["./*"],
"@modelcontextprotocol/core-internal": ["./node_modules/@modelcontextprotocol/core-internal/src/index.ts"],
"@modelcontextprotocol/core/internal": ["./node_modules/@modelcontextprotocol/core/src/internal.ts"],
"@modelcontextprotocol/core-internal/public": [
"./node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts"
],

View file

@ -34,5 +34,8 @@ export default defineConfig({
}
},
noExternal: ['@modelcontextprotocol/core-internal', 'ajv', 'ajv-formats', '@cfworker/json-schema'],
external: ['@modelcontextprotocol/client/_shims']
// The schema modules live in @modelcontextprotocol/core (a real runtime dependency); the
// bundled core-internal shims import them via the './internal' subpath, which must stay an
// external import (explicit entry — the tsconfig paths alias would otherwise inline it).
external: ['@modelcontextprotocol/client/_shims', '@modelcontextprotocol/core/internal']
});

View file

@ -12,9 +12,9 @@ describe('AUTH_SCHEMA_NAMES (codemod auth schema-routing allowlist)', () => {
// the rewritten import would have no exported member. AUTH_SCHEMA_NAMES is the v1 auth-schema set,
// a SUBSET of core's auth exports: core may export more (v2-only schemas such as
// IdJagTokenExchangeResponseSchema) that v1 never had and the codemod never encounters. Read
// core's barrel directly (the `export { … } from '…/core-internal/auth'` block) so they cannot drift.
// core's barrel directly (the `export { … } from './auth'` block) so they cannot drift.
const src = readFileSync(fileURLToPath(new URL('../../../core/src/index.ts', import.meta.url)), 'utf8');
const closeIdx = src.indexOf("} from '@modelcontextprotocol/core-internal/auth'");
const closeIdx = src.indexOf("} from './auth'");
const openIdx = src.lastIndexOf('export {', closeIdx);
const block = src.slice(openIdx + 'export {'.length, closeIdx);
const coreAuthExports = new Set([...block.matchAll(/\b(\w+Schema)\b/g)].map(m => m[1]));

View file

@ -42,11 +42,17 @@ export default [
{
patterns: [
{
group: ['**/types/schemas', '**/types/schemas.js'],
group: [
'**/types/schemas',
'**/types/schemas.js',
'@modelcontextprotocol/core',
'@modelcontextprotocol/core/*'
],
allowTypeImports: true,
message:
'Wire revision modules must be self-contained. Freeze a copy of the schema into the ' +
'rev*/ directory instead of importing the mutable public-layer types/schemas.ts.'
'rev*/ directory instead of importing the mutable public-layer schemas ' +
'(types/schemas.ts or @modelcontextprotocol/core).'
}
]
}

View file

@ -51,6 +51,7 @@
"test:watch": "vitest"
},
"dependencies": {
"@modelcontextprotocol/core": "workspace:*",
"content-type": "catalog:runtimeShared",
"json-schema-typed": "catalog:runtimeShared",
"zod": "catalog:runtimeShared"

View file

@ -1,285 +1,39 @@
import * as z from 'zod/v4';
/**
* Reusable URL validation that disallows `javascript:` scheme
*/
export const SafeUrlSchema = z
.url()
.superRefine((val, ctx) => {
if (!URL.canParse(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'URL must be parseable',
fatal: true
});
return z.NEVER;
}
})
.refine(
url => {
const u = new URL(url);
return u.protocol !== 'javascript:' && u.protocol !== 'data:' && u.protocol !== 'vbscript:';
},
{ message: 'URL cannot use javascript:, data:, or vbscript: scheme' }
);
/**
* RFC 9728 OAuth Protected Resource Metadata
*/
export const OAuthProtectedResourceMetadataSchema = z.looseObject({
resource: z.string().url(),
authorization_servers: z.array(SafeUrlSchema).optional(),
jwks_uri: z.string().url().optional(),
scopes_supported: z.array(z.string()).optional(),
bearer_methods_supported: z.array(z.string()).optional(),
resource_signing_alg_values_supported: z.array(z.string()).optional(),
resource_name: z.string().optional(),
resource_documentation: z.string().optional(),
resource_policy_uri: z.string().url().optional(),
resource_tos_uri: z.string().url().optional(),
tls_client_certificate_bound_access_tokens: z.boolean().optional(),
authorization_details_types_supported: z.array(z.string()).optional(),
dpop_signing_alg_values_supported: z.array(z.string()).optional(),
dpop_bound_access_tokens_required: z.boolean().optional()
});
/**
* RFC 8414 OAuth 2.0 Authorization Server Metadata
*/
export const OAuthMetadataSchema = z.looseObject({
issuer: z.string(),
authorization_endpoint: SafeUrlSchema,
token_endpoint: SafeUrlSchema,
registration_endpoint: SafeUrlSchema.optional(),
scopes_supported: z.array(z.string()).optional(),
response_types_supported: z.array(z.string()),
response_modes_supported: z.array(z.string()).optional(),
grant_types_supported: z.array(z.string()).optional(),
token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
service_documentation: SafeUrlSchema.optional(),
revocation_endpoint: SafeUrlSchema.optional(),
revocation_endpoint_auth_methods_supported: z.array(z.string()).optional(),
revocation_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
introspection_endpoint: z.string().optional(),
introspection_endpoint_auth_methods_supported: z.array(z.string()).optional(),
introspection_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
code_challenge_methods_supported: z.array(z.string()).optional(),
client_id_metadata_document_supported: z.boolean().optional(),
// eslint-disable-next-line unicorn/prefer-top-level-await -- Zod .catch(), not a Promise chain
authorization_response_iss_parameter_supported: z.boolean().optional().catch(undefined)
});
/**
* OpenID Connect Discovery 1.0 Provider Metadata
*
* @see https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
*/
export const OpenIdProviderMetadataSchema = z.looseObject({
issuer: z.string(),
authorization_endpoint: SafeUrlSchema,
token_endpoint: SafeUrlSchema,
userinfo_endpoint: SafeUrlSchema.optional(),
jwks_uri: SafeUrlSchema,
registration_endpoint: SafeUrlSchema.optional(),
scopes_supported: z.array(z.string()).optional(),
response_types_supported: z.array(z.string()),
response_modes_supported: z.array(z.string()).optional(),
grant_types_supported: z.array(z.string()).optional(),
acr_values_supported: z.array(z.string()).optional(),
subject_types_supported: z.array(z.string()),
id_token_signing_alg_values_supported: z.array(z.string()),
id_token_encryption_alg_values_supported: z.array(z.string()).optional(),
id_token_encryption_enc_values_supported: z.array(z.string()).optional(),
userinfo_signing_alg_values_supported: z.array(z.string()).optional(),
userinfo_encryption_alg_values_supported: z.array(z.string()).optional(),
userinfo_encryption_enc_values_supported: z.array(z.string()).optional(),
request_object_signing_alg_values_supported: z.array(z.string()).optional(),
request_object_encryption_alg_values_supported: z.array(z.string()).optional(),
request_object_encryption_enc_values_supported: z.array(z.string()).optional(),
token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
display_values_supported: z.array(z.string()).optional(),
claim_types_supported: z.array(z.string()).optional(),
claims_supported: z.array(z.string()).optional(),
service_documentation: z.string().optional(),
claims_locales_supported: z.array(z.string()).optional(),
ui_locales_supported: z.array(z.string()).optional(),
claims_parameter_supported: z.boolean().optional(),
request_parameter_supported: z.boolean().optional(),
request_uri_parameter_supported: z.boolean().optional(),
require_request_uri_registration: z.boolean().optional(),
op_policy_uri: SafeUrlSchema.optional(),
op_tos_uri: SafeUrlSchema.optional(),
client_id_metadata_document_supported: z.boolean().optional(),
// eslint-disable-next-line unicorn/prefer-top-level-await -- Zod .catch(), not a Promise chain
authorization_response_iss_parameter_supported: z.boolean().optional().catch(undefined)
});
/**
* OpenID Connect Discovery metadata that may include OAuth 2.0 fields
* This schema represents the real-world scenario where OIDC providers
* return a mix of OpenID Connect and OAuth 2.0 metadata fields
*/
export const OpenIdProviderDiscoveryMetadataSchema = z.object({
...OpenIdProviderMetadataSchema.shape,
...OAuthMetadataSchema.pick({
code_challenge_methods_supported: true
}).shape
});
/**
* OAuth 2.1 token response
*/
export const OAuthTokensSchema = z
.object({
access_token: z.string(),
id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect
token_type: z.string(),
expires_in: z.coerce.number().optional(),
scope: z.string().optional(),
refresh_token: z.string().optional()
})
.strip();
/**
* RFC 8693 §2.2.1 Token Exchange response for ID-JAG tokens.
*
* `token_type` is intentionally optional: per RFC 8693 §2.2.1 it is informational when
* the issued token is not an access token, and per RFC 6749 §5.1 it is case-insensitive,
* so strict checking rejects conformant IdPs.
*/
export const IdJagTokenExchangeResponseSchema = z
.object({
issued_token_type: z.literal('urn:ietf:params:oauth:token-type:id-jag'),
access_token: z.string(),
token_type: z.string().optional(),
expires_in: z.number().optional(),
scope: z.string().optional()
})
.strip();
export type IdJagTokenExchangeResponse = z.infer<typeof IdJagTokenExchangeResponseSchema>;
/**
* OAuth 2.1 error response
*/
export const OAuthErrorResponseSchema = z.object({
error: z.string(),
error_description: z.string().optional(),
error_uri: z.string().optional()
});
/**
* Optional version of {@linkcode SafeUrlSchema} that allows empty string for backward compatibility on `tos_uri` and `logo_uri`
*/
// eslint-disable-next-line unicorn/no-useless-undefined
export const OptionalSafeUrlSchema = SafeUrlSchema.optional().or(z.literal('').transform(() => undefined));
/**
* RFC 7591 OAuth 2.0 Dynamic Client Registration metadata
*/
export const OAuthClientMetadataSchema = z
.object({
redirect_uris: z.array(SafeUrlSchema),
token_endpoint_auth_method: z.string().optional(),
grant_types: z.array(z.string()).optional(),
response_types: z.array(z.string()).optional(),
/**
* OIDC Dynamic Client Registration `application_type`. MCP clients MUST set
* this to `'native'` or `'web'` when registering (SEP-837); the SDK defaults
* it from `redirect_uris` when omitted. Typed as `string` (not an enum) so
* that parsing an authorization server's registration response — which under
* RFC 7591 may echo extension values — never rejects the document on this
* field alone.
*/
application_type: z.string().optional(),
client_name: z.string().optional(),
client_uri: SafeUrlSchema.optional(),
logo_uri: OptionalSafeUrlSchema,
scope: z.string().optional(),
contacts: z.array(z.string()).optional(),
tos_uri: OptionalSafeUrlSchema,
policy_uri: z.string().optional(),
jwks_uri: SafeUrlSchema.optional(),
jwks: z.any().optional(),
software_id: z.string().optional(),
software_version: z.string().optional(),
software_statement: z.string().optional()
})
.strip();
/**
* RFC 7591 OAuth 2.0 Dynamic Client Registration client information
*/
export const OAuthClientInformationSchema = z
.object({
client_id: z.string(),
client_secret: z.string().optional(),
client_id_issued_at: z.number().optional(),
client_secret_expires_at: z.number().optional()
})
.strip();
/**
* RFC 7591 OAuth 2.0 Dynamic Client Registration full response (client information plus metadata)
*/
export const OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge(OAuthClientInformationSchema);
/**
* RFC 7591 OAuth 2.0 Dynamic Client Registration error response
*/
export const OAuthClientRegistrationErrorSchema = z
.object({
error: z.string(),
error_description: z.string().optional()
})
.strip();
/**
* RFC 7009 OAuth 2.0 Token Revocation request
*/
export const OAuthTokenRevocationRequestSchema = z
.object({
token: z.string(),
token_type_hint: z.string().optional()
})
.strip();
export type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>;
export type OpenIdProviderMetadata = z.infer<typeof OpenIdProviderMetadataSchema>;
export type OpenIdProviderDiscoveryMetadata = z.infer<typeof OpenIdProviderDiscoveryMetadataSchema>;
export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
export type OAuthErrorResponse = z.infer<typeof OAuthErrorResponseSchema>;
export type OAuthClientMetadata = z.infer<typeof OAuthClientMetadataSchema>;
export type OAuthClientInformation = z.infer<typeof OAuthClientInformationSchema>;
export type OAuthClientInformationFull = z.infer<typeof OAuthClientInformationFullSchema>;
export type OAuthClientInformationMixed = OAuthClientInformation | OAuthClientInformationFull;
/**
* {@linkcode OAuthTokens} as persisted by an `OAuthClientProvider`. Adds an
* SDK-stamped authorization-server `issuer` identifier so stored tokens are
* bound to the AS that issued them. The `issuer` field is **not** part of the
* RFC 6749 wire response and is intentionally absent from the wire-response
* schema; the client SDK writes it before calling `saveTokens`.
*/
export type StoredOAuthTokens = OAuthTokens & { issuer?: string };
/**
* {@linkcode OAuthClientInformationMixed} as persisted by an
* `OAuthClientProvider`. Adds an SDK-stamped authorization-server `issuer`
* identifier so stored client credentials are bound to the AS that issued them.
* The `issuer` field is **not** part of the RFC 7591 wire response and is
* intentionally absent from the wire-response schema; the client SDK writes it
* before calling `saveClientInformation`.
*/
export type StoredOAuthClientInformation = OAuthClientInformationMixed & { issuer?: string };
export type OAuthClientRegistrationError = z.infer<typeof OAuthClientRegistrationErrorSchema>;
export type OAuthTokenRevocationRequest = z.infer<typeof OAuthTokenRevocationRequestSchema>;
export type OAuthProtectedResourceMetadata = z.infer<typeof OAuthProtectedResourceMetadataSchema>;
// Unified type for authorization server metadata
export type AuthorizationServerMetadata = OAuthMetadata | OpenIdProviderDiscoveryMetadata;
// Moved: the OAuth/OpenID schemas + types now live in @modelcontextprotocol/core (packages/core/src/auth.ts).
// This module re-exports them one-to-one so every existing import path keeps working.
//
// Add new schemas in packages/core/src/auth.ts, never here — this file only forwards, and
// core-internal's schemaShims test enforces that it stays free of zod imports and definitions.
export type {
AuthorizationServerMetadata,
IdJagTokenExchangeResponse,
OAuthClientInformation,
OAuthClientInformationFull,
OAuthClientInformationMixed,
OAuthClientMetadata,
OAuthClientRegistrationError,
OAuthErrorResponse,
OAuthMetadata,
OAuthProtectedResourceMetadata,
OAuthTokenRevocationRequest,
OAuthTokens,
OpenIdProviderDiscoveryMetadata,
OpenIdProviderMetadata,
StoredOAuthClientInformation,
StoredOAuthTokens
} from '@modelcontextprotocol/core/internal';
export {
IdJagTokenExchangeResponseSchema,
OAuthClientInformationFullSchema,
OAuthClientInformationSchema,
OAuthClientMetadataSchema,
OAuthClientRegistrationErrorSchema,
OAuthErrorResponseSchema,
OAuthMetadataSchema,
OAuthProtectedResourceMetadataSchema,
OAuthTokenRevocationRequestSchema,
OAuthTokensSchema,
OpenIdProviderDiscoveryMetadataSchema,
OpenIdProviderMetadataSchema,
OptionalSafeUrlSchema,
SafeUrlSchema
} from '@modelcontextprotocol/core/internal';

View file

@ -1,104 +1,25 @@
export const LATEST_PROTOCOL_VERSION = '2025-11-25';
export const DEFAULT_NEGOTIATED_PROTOCOL_VERSION = '2025-03-26';
export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07'];
/**
* `_meta` key associating a message with a 2025-11-25 task.
*
* @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only.
*/
export const RELATED_TASK_META_KEY = 'io.modelcontextprotocol/related-task';
/* Reserved `_meta` keys for the per-request envelope (protocol revision 2026-07-28) */
/**
* `_meta` key carrying the MCP protocol version governing a request.
*
* For the HTTP transport, the value must match the `MCP-Protocol-Version` header.
*/
export const PROTOCOL_VERSION_META_KEY = 'io.modelcontextprotocol/protocolVersion';
/**
* `_meta` key identifying the client software making a request.
*/
export const CLIENT_INFO_META_KEY = 'io.modelcontextprotocol/clientInfo';
/**
* `_meta` key carrying the client's capabilities for a request.
*
* Capabilities are declared per request rather than once at initialization;
* servers must not infer capabilities from prior requests.
*/
export const CLIENT_CAPABILITIES_META_KEY = 'io.modelcontextprotocol/clientCapabilities';
/**
* `_meta` key carrying the JSON-RPC ID of the `subscriptions/listen` request
* that opened the stream a notification was delivered on.
*
* Stamped by the server on every notification delivered via a
* `subscriptions/listen` stream (including the leading
* `notifications/subscriptions/acknowledged`); on stdio, where all messages
* share one channel, clients use it to correlate notifications with their
* originating subscription. The value is the listen request's JSON-RPC ID
* verbatim.
*/
export const SUBSCRIPTION_ID_META_KEY = 'io.modelcontextprotocol/subscriptionId';
/**
* `_meta` key carrying the desired log level for a request.
*
* When absent, the server must not send `notifications/message` notifications
* for the request.
*
* @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains
* in the specification for at least twelve months.
*/
export const LOG_LEVEL_META_KEY = 'io.modelcontextprotocol/logLevel';
/*
* Reserved `_meta` keys for distributed trace context propagation (SEP-414).
*
* These unprefixed keys are reserved by the MCP specification as an explicit
* exception to the `_meta` key prefix rule. The SDK does not interpret them;
* they pass through `_meta` untouched for OpenTelemetry-style propagation.
*/
/**
* `_meta` key carrying W3C Trace Context for distributed tracing (SEP-414).
*
* When present, the value MUST follow the W3C `traceparent` header format,
* e.g. `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`.
*
* @see https://www.w3.org/TR/trace-context/#traceparent-header
*/
export const TRACEPARENT_META_KEY = 'traceparent';
/**
* `_meta` key carrying vendor-specific trace state for distributed tracing (SEP-414).
*
* When present, the value MUST follow the W3C `tracestate` header format,
* e.g. `vendor1=value1,vendor2=value2`.
*
* @see https://www.w3.org/TR/trace-context/#tracestate-header
*/
export const TRACESTATE_META_KEY = 'tracestate';
/**
* `_meta` key carrying cross-cutting propagation values for distributed tracing (SEP-414).
*
* When present, the value MUST follow the W3C Baggage header format,
* e.g. `userId=alice,serverRegion=us-east-1`.
*
* @see https://www.w3.org/TR/baggage/
*/
export const BAGGAGE_META_KEY = 'baggage';
/* JSON-RPC types */
export const JSONRPC_VERSION = '2.0';
/* Standard JSON-RPC error code constants */
export const PARSE_ERROR = -32_700;
export const INVALID_REQUEST = -32_600;
export const METHOD_NOT_FOUND = -32_601;
export const INVALID_PARAMS = -32_602;
export const INTERNAL_ERROR = -32_603;
// Moved: the protocol constants now live in @modelcontextprotocol/core (packages/core/src/constants.ts).
// This module re-exports them one-to-one so every existing import path keeps working.
//
// Add new constants in packages/core/src/constants.ts, never here — this file only forwards, and
// core-internal's schemaShims test enforces that it stays free of zod imports and definitions.
export {
BAGGAGE_META_KEY,
CLIENT_CAPABILITIES_META_KEY,
CLIENT_INFO_META_KEY,
DEFAULT_NEGOTIATED_PROTOCOL_VERSION,
INTERNAL_ERROR,
INVALID_PARAMS,
INVALID_REQUEST,
JSONRPC_VERSION,
LATEST_PROTOCOL_VERSION,
LOG_LEVEL_META_KEY,
METHOD_NOT_FOUND,
PARSE_ERROR,
PROTOCOL_VERSION_META_KEY,
RELATED_TASK_META_KEY,
SUBSCRIPTION_ID_META_KEY,
SUPPORTED_PROTOCOL_VERSIONS,
TRACEPARENT_META_KEY,
TRACESTATE_META_KEY
} from '@modelcontextprotocol/core/internal';

File diff suppressed because it is too large Load diff

View file

@ -175,10 +175,8 @@ import type {
UntitledSingleSelectEnumSchemaSchema
} from './schemas';
/* JSON types */
export type JSONValue = string | number | boolean | null | JSONObject | JSONArray;
export type JSONObject = { [key: string]: JSONValue };
export type JSONArray = JSONValue[];
/* JSON types — moved to @modelcontextprotocol/core (packages/core/src/types.ts). */
export type { JSONArray, JSONObject, JSONValue } from '@modelcontextprotocol/core/internal';
/**
* Utility types

View file

@ -53,7 +53,10 @@ const PUBLIC_PACKAGES: Record<string, { name: string; exportKeys: string[]; bin?
'middleware/node': { name: '@modelcontextprotocol/node', exportKeys: ['.'] },
core: {
name: '@modelcontextprotocol/core',
exportKeys: ['.']
// './internal' is the wholesale internal seam the sibling SDK packages resolve at
// runtime (their bundles keep `@modelcontextprotocol/core/internal` imports external);
// it is not public API — the curated public surface stays the root entry.
exportKeys: ['.', './internal']
},
codemod: {
name: '@modelcontextprotocol/codemod',

View file

@ -0,0 +1,59 @@
/**
* Shim purity pins: the schema-module re-export shims stay forwarding-only.
*
* The neutral schema sources live in @modelcontextprotocol/core
* (packages/core/src/{schemas,auth,constants}.ts); core-internal keeps the old
* module paths only as one-to-one re-export shims. If someone adds a new Zod
* schema to a shim instead of to core, the name exists at the old path but
* never reaches core's published entries — the exact drift the move was meant
* to end. This pins the shims to pure forwarding: no zod import, no schema or
* constant definitions, imports only from @modelcontextprotocol/core/internal.
*/
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, test } from 'vitest';
const srcDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'src');
const SHIMS = ['types/schemas.ts', 'shared/auth.ts', 'types/constants.ts'];
const NEW_HOME = 'new schemas/constants belong in packages/core/src (schemas.ts, auth.ts, constants.ts), not in the re-export shims';
/** Drop comments so header prose (which may mention `const`, zod, etc.) can't trip the code checks. */
function stripComments(source: string): string {
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
}
describe('core-internal schema shims only forward to @modelcontextprotocol/core', () => {
for (const shim of SHIMS) {
describe(shim, () => {
const source = stripComments(readFileSync(join(srcDir, shim), 'utf8'));
test('does not import zod', () => {
expect(source, `${shim} imports zod — ${NEW_HOME}`).not.toMatch(/from\s+['"]zod/);
});
test('defines no schemas or constants', () => {
expect(source, `${shim} builds a Zod schema — ${NEW_HOME}`).not.toMatch(/\bz\s*\.\s*[a-zA-Z]/);
expect(source, `${shim} declares a local binding — ${NEW_HOME}`).not.toMatch(
/\b(?:const|let|var|function|class|enum|interface)\s/
);
});
test('imports only from @modelcontextprotocol/core/internal', () => {
for (const m of source.matchAll(/from\s+['"]([^'"]+)['"]/g)) {
expect(m[1], `${shim} forwards from an unexpected module — ${NEW_HOME}`).toBe('@modelcontextprotocol/core/internal');
}
});
});
}
test('types.ts re-exports the JSON value types from core as type-only', () => {
const source = readFileSync(join(srcDir, 'types/types.ts'), 'utf8');
expect(source).toMatch(/export type \{ JSONArray, JSONObject, JSONValue \} from '@modelcontextprotocol\/core\/internal';/);
// A value re-export would make types.ts depend on core's runtime; keep it erasable.
expect(source).not.toMatch(/export \{[^}]*JSON(?:Array|Object|Value)\b[^}]*\} from/);
});
});

View file

@ -135,7 +135,8 @@ describe('task vocabulary is importable but in no API signature', () => {
// codec split (Q1 increment 2); the param-side carriers stay in the
// neutral file. Both homes are scanned — the combined surface is the
// same ≥19 schemas the docs claim covers.
const neutral = readFileSync(join(__dirname, '..', '..', 'src', 'types', 'schemas.ts'), 'utf8');
// The neutral schema source moved to @modelcontextprotocol/core (the old path is a re-export shim).
const neutral = readFileSync(join(__dirname, '..', '..', '..', 'core', 'src', 'schemas.ts'), 'utf8');
const wire2025 = readFileSync(join(__dirname, '..', '..', 'src', 'wire', 'rev2025-11-25', 'schemas.ts'), 'utf8');
let total = 0;
for (const schemas of [neutral, wire2025]) {
@ -159,7 +160,7 @@ describe('task vocabulary is importable but in no API signature', () => {
);
}
const constants = readFileSync(join(__dirname, '..', '..', 'src', 'types', 'constants.ts'), 'utf8');
const constants = readFileSync(join(__dirname, '..', '..', '..', 'core', 'src', 'constants.ts'), 'utf8');
const keyDecl = constants.indexOf('export const RELATED_TASK_META_KEY');
expect(constants.slice(Math.max(0, keyDecl - 300), keyDecl)).toContain('@deprecated');
});

View file

@ -46,8 +46,9 @@ function read(rel: string): string {
/** Matches any import/re-export whose module specifier contains `wire/rev`. */
const WIRE_REV_IMPORT = /^(import|export)\b[^;]*?\bfrom\s+['"][^'"]*wire\/rev[^'"]*['"]/m;
/** Matches a runtime (non-type-only) import or re-export of the public schemas module. */
const RUNTIME_SCHEMAS_IMPORT = /^(import|export)\s+(?!type\b)[^;]*?\bfrom\s+['"][^'"]*types\/schemas(?:\.js)?['"]/m;
/** Matches a runtime (non-type-only) import or re-export of the public schemas module (old path or its core home). */
const RUNTIME_SCHEMAS_IMPORT =
/^(import|export)\s+(?!type\b)[^;]*?\bfrom\s+['"](?:[^'"]*types\/schemas(?:\.js)?|@modelcontextprotocol\/core(?:\/internal)?)['"]/m;
/** Matches an eslint-disable directive that touches a no-restricted-imports rule. */
const DISABLE_RESTRICTED = /eslint-disable[^\n]*no-restricted-imports/;

View file

@ -6,7 +6,8 @@
"paths": {
"*": ["./*"],
"@modelcontextprotocol/eslint-config": ["./node_modules/@modelcontextprotocol/eslint-config/tsconfig.json"],
"@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"]
"@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"],
"@modelcontextprotocol/core/internal": ["./node_modules/@modelcontextprotocol/core/src/internal.ts"]
}
}
}

View file

@ -30,6 +30,23 @@
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./internal": {
"import": {
"types": "./dist/internal.d.mts",
"default": "./dist/internal.mjs"
},
"require": {
"types": "./dist/internal.d.cts",
"default": "./dist/internal.cjs"
}
}
},
"typesVersions": {
"*": {
"internal": [
"dist/internal.d.mts"
]
}
},
"main": "./dist/index.cjs",
@ -53,7 +70,6 @@
},
"devDependencies": {
"@eslint/js": "catalog:devTools",
"@modelcontextprotocol/core-internal": "workspace:^",
"@modelcontextprotocol/eslint-config": "workspace:^",
"@modelcontextprotocol/tsconfig": "workspace:^",
"@modelcontextprotocol/vitest-config": "workspace:^",

285
packages/core/src/auth.ts Normal file
View file

@ -0,0 +1,285 @@
import * as z from 'zod/v4';
/**
* Reusable URL validation that disallows `javascript:` scheme
*/
export const SafeUrlSchema = z
.url()
.superRefine((val, ctx) => {
if (!URL.canParse(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'URL must be parseable',
fatal: true
});
return z.NEVER;
}
})
.refine(
url => {
const u = new URL(url);
return u.protocol !== 'javascript:' && u.protocol !== 'data:' && u.protocol !== 'vbscript:';
},
{ message: 'URL cannot use javascript:, data:, or vbscript: scheme' }
);
/**
* RFC 9728 OAuth Protected Resource Metadata
*/
export const OAuthProtectedResourceMetadataSchema = z.looseObject({
resource: z.string().url(),
authorization_servers: z.array(SafeUrlSchema).optional(),
jwks_uri: z.string().url().optional(),
scopes_supported: z.array(z.string()).optional(),
bearer_methods_supported: z.array(z.string()).optional(),
resource_signing_alg_values_supported: z.array(z.string()).optional(),
resource_name: z.string().optional(),
resource_documentation: z.string().optional(),
resource_policy_uri: z.string().url().optional(),
resource_tos_uri: z.string().url().optional(),
tls_client_certificate_bound_access_tokens: z.boolean().optional(),
authorization_details_types_supported: z.array(z.string()).optional(),
dpop_signing_alg_values_supported: z.array(z.string()).optional(),
dpop_bound_access_tokens_required: z.boolean().optional()
});
/**
* RFC 8414 OAuth 2.0 Authorization Server Metadata
*/
export const OAuthMetadataSchema = z.looseObject({
issuer: z.string(),
authorization_endpoint: SafeUrlSchema,
token_endpoint: SafeUrlSchema,
registration_endpoint: SafeUrlSchema.optional(),
scopes_supported: z.array(z.string()).optional(),
response_types_supported: z.array(z.string()),
response_modes_supported: z.array(z.string()).optional(),
grant_types_supported: z.array(z.string()).optional(),
token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
service_documentation: SafeUrlSchema.optional(),
revocation_endpoint: SafeUrlSchema.optional(),
revocation_endpoint_auth_methods_supported: z.array(z.string()).optional(),
revocation_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
introspection_endpoint: z.string().optional(),
introspection_endpoint_auth_methods_supported: z.array(z.string()).optional(),
introspection_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
code_challenge_methods_supported: z.array(z.string()).optional(),
client_id_metadata_document_supported: z.boolean().optional(),
// eslint-disable-next-line unicorn/prefer-top-level-await -- Zod .catch(), not a Promise chain
authorization_response_iss_parameter_supported: z.boolean().optional().catch(undefined)
});
/**
* OpenID Connect Discovery 1.0 Provider Metadata
*
* @see https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
*/
export const OpenIdProviderMetadataSchema = z.looseObject({
issuer: z.string(),
authorization_endpoint: SafeUrlSchema,
token_endpoint: SafeUrlSchema,
userinfo_endpoint: SafeUrlSchema.optional(),
jwks_uri: SafeUrlSchema,
registration_endpoint: SafeUrlSchema.optional(),
scopes_supported: z.array(z.string()).optional(),
response_types_supported: z.array(z.string()),
response_modes_supported: z.array(z.string()).optional(),
grant_types_supported: z.array(z.string()).optional(),
acr_values_supported: z.array(z.string()).optional(),
subject_types_supported: z.array(z.string()),
id_token_signing_alg_values_supported: z.array(z.string()),
id_token_encryption_alg_values_supported: z.array(z.string()).optional(),
id_token_encryption_enc_values_supported: z.array(z.string()).optional(),
userinfo_signing_alg_values_supported: z.array(z.string()).optional(),
userinfo_encryption_alg_values_supported: z.array(z.string()).optional(),
userinfo_encryption_enc_values_supported: z.array(z.string()).optional(),
request_object_signing_alg_values_supported: z.array(z.string()).optional(),
request_object_encryption_alg_values_supported: z.array(z.string()).optional(),
request_object_encryption_enc_values_supported: z.array(z.string()).optional(),
token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(),
display_values_supported: z.array(z.string()).optional(),
claim_types_supported: z.array(z.string()).optional(),
claims_supported: z.array(z.string()).optional(),
service_documentation: z.string().optional(),
claims_locales_supported: z.array(z.string()).optional(),
ui_locales_supported: z.array(z.string()).optional(),
claims_parameter_supported: z.boolean().optional(),
request_parameter_supported: z.boolean().optional(),
request_uri_parameter_supported: z.boolean().optional(),
require_request_uri_registration: z.boolean().optional(),
op_policy_uri: SafeUrlSchema.optional(),
op_tos_uri: SafeUrlSchema.optional(),
client_id_metadata_document_supported: z.boolean().optional(),
// eslint-disable-next-line unicorn/prefer-top-level-await -- Zod .catch(), not a Promise chain
authorization_response_iss_parameter_supported: z.boolean().optional().catch(undefined)
});
/**
* OpenID Connect Discovery metadata that may include OAuth 2.0 fields
* This schema represents the real-world scenario where OIDC providers
* return a mix of OpenID Connect and OAuth 2.0 metadata fields
*/
export const OpenIdProviderDiscoveryMetadataSchema = z.object({
...OpenIdProviderMetadataSchema.shape,
...OAuthMetadataSchema.pick({
code_challenge_methods_supported: true
}).shape
});
/**
* OAuth 2.1 token response
*/
export const OAuthTokensSchema = z
.object({
access_token: z.string(),
id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect
token_type: z.string(),
expires_in: z.coerce.number().optional(),
scope: z.string().optional(),
refresh_token: z.string().optional()
})
.strip();
/**
* RFC 8693 §2.2.1 Token Exchange response for ID-JAG tokens.
*
* `token_type` is intentionally optional: per RFC 8693 §2.2.1 it is informational when
* the issued token is not an access token, and per RFC 6749 §5.1 it is case-insensitive,
* so strict checking rejects conformant IdPs.
*/
export const IdJagTokenExchangeResponseSchema = z
.object({
issued_token_type: z.literal('urn:ietf:params:oauth:token-type:id-jag'),
access_token: z.string(),
token_type: z.string().optional(),
expires_in: z.number().optional(),
scope: z.string().optional()
})
.strip();
export type IdJagTokenExchangeResponse = z.infer<typeof IdJagTokenExchangeResponseSchema>;
/**
* OAuth 2.1 error response
*/
export const OAuthErrorResponseSchema = z.object({
error: z.string(),
error_description: z.string().optional(),
error_uri: z.string().optional()
});
/**
* Optional version of {@linkcode SafeUrlSchema} that allows empty string for backward compatibility on `tos_uri` and `logo_uri`
*/
// eslint-disable-next-line unicorn/no-useless-undefined
export const OptionalSafeUrlSchema = SafeUrlSchema.optional().or(z.literal('').transform(() => undefined));
/**
* RFC 7591 OAuth 2.0 Dynamic Client Registration metadata
*/
export const OAuthClientMetadataSchema = z
.object({
redirect_uris: z.array(SafeUrlSchema),
token_endpoint_auth_method: z.string().optional(),
grant_types: z.array(z.string()).optional(),
response_types: z.array(z.string()).optional(),
/**
* OIDC Dynamic Client Registration `application_type`. MCP clients MUST set
* this to `'native'` or `'web'` when registering (SEP-837); the SDK defaults
* it from `redirect_uris` when omitted. Typed as `string` (not an enum) so
* that parsing an authorization server's registration response — which under
* RFC 7591 may echo extension values — never rejects the document on this
* field alone.
*/
application_type: z.string().optional(),
client_name: z.string().optional(),
client_uri: SafeUrlSchema.optional(),
logo_uri: OptionalSafeUrlSchema,
scope: z.string().optional(),
contacts: z.array(z.string()).optional(),
tos_uri: OptionalSafeUrlSchema,
policy_uri: z.string().optional(),
jwks_uri: SafeUrlSchema.optional(),
jwks: z.any().optional(),
software_id: z.string().optional(),
software_version: z.string().optional(),
software_statement: z.string().optional()
})
.strip();
/**
* RFC 7591 OAuth 2.0 Dynamic Client Registration client information
*/
export const OAuthClientInformationSchema = z
.object({
client_id: z.string(),
client_secret: z.string().optional(),
client_id_issued_at: z.number().optional(),
client_secret_expires_at: z.number().optional()
})
.strip();
/**
* RFC 7591 OAuth 2.0 Dynamic Client Registration full response (client information plus metadata)
*/
export const OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge(OAuthClientInformationSchema);
/**
* RFC 7591 OAuth 2.0 Dynamic Client Registration error response
*/
export const OAuthClientRegistrationErrorSchema = z
.object({
error: z.string(),
error_description: z.string().optional()
})
.strip();
/**
* RFC 7009 OAuth 2.0 Token Revocation request
*/
export const OAuthTokenRevocationRequestSchema = z
.object({
token: z.string(),
token_type_hint: z.string().optional()
})
.strip();
export type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>;
export type OpenIdProviderMetadata = z.infer<typeof OpenIdProviderMetadataSchema>;
export type OpenIdProviderDiscoveryMetadata = z.infer<typeof OpenIdProviderDiscoveryMetadataSchema>;
export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
export type OAuthErrorResponse = z.infer<typeof OAuthErrorResponseSchema>;
export type OAuthClientMetadata = z.infer<typeof OAuthClientMetadataSchema>;
export type OAuthClientInformation = z.infer<typeof OAuthClientInformationSchema>;
export type OAuthClientInformationFull = z.infer<typeof OAuthClientInformationFullSchema>;
export type OAuthClientInformationMixed = OAuthClientInformation | OAuthClientInformationFull;
/**
* {@linkcode OAuthTokens} as persisted by an `OAuthClientProvider`. Adds an
* SDK-stamped authorization-server `issuer` identifier so stored tokens are
* bound to the AS that issued them. The `issuer` field is **not** part of the
* RFC 6749 wire response and is intentionally absent from the wire-response
* schema; the client SDK writes it before calling `saveTokens`.
*/
export type StoredOAuthTokens = OAuthTokens & { issuer?: string };
/**
* {@linkcode OAuthClientInformationMixed} as persisted by an
* `OAuthClientProvider`. Adds an SDK-stamped authorization-server `issuer`
* identifier so stored client credentials are bound to the AS that issued them.
* The `issuer` field is **not** part of the RFC 7591 wire response and is
* intentionally absent from the wire-response schema; the client SDK writes it
* before calling `saveClientInformation`.
*/
export type StoredOAuthClientInformation = OAuthClientInformationMixed & { issuer?: string };
export type OAuthClientRegistrationError = z.infer<typeof OAuthClientRegistrationErrorSchema>;
export type OAuthTokenRevocationRequest = z.infer<typeof OAuthTokenRevocationRequestSchema>;
export type OAuthProtectedResourceMetadata = z.infer<typeof OAuthProtectedResourceMetadataSchema>;
// Unified type for authorization server metadata
export type AuthorizationServerMetadata = OAuthMetadata | OpenIdProviderDiscoveryMetadata;

Some files were not shown because too many files have changed in this diff Show more