feat: implement auth/pre-registration conformance scenario (#1545)

This commit is contained in:
Felix Weinberger 2026-02-16 19:27:46 +00:00 • committed by GitHub
commit f2d21458cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 265 additions and 27 deletions

View file

@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: npm
- run: npm ci
- run: npm run build
@ -33,7 +33,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: npm
- run: npm ci
- run: npm run build

208
package-lock.json generated
View file

@ -30,7 +30,7 @@
"devDependencies": {
"@cfworker/json-schema": "^4.1.1",
"@eslint/js": "^9.39.1",
"@modelcontextprotocol/conformance": "^0.1.11",
"@modelcontextprotocol/conformance": "^0.1.14",
"@types/content-type": "^1.1.8",
"@types/cors": "^2.8.17",
"@types/cross-spawn": "^6.0.6",
@ -737,13 +737,14 @@
"license": "MIT"
},
"node_modules/@modelcontextprotocol/conformance": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/conformance/-/conformance-0.1.11.tgz",
"integrity": "sha512-cqayAmyTUhnRsyrOuTqZ+kCc2w/goppxnqZ+XrOsVd/M25No/HiZ1GbZI92sFA7ONYzonqRja56G9IiISIns3A==",
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/conformance/-/conformance-0.1.14.tgz",
"integrity": "sha512-CNl/7d+yHfXExPDUsNG/kO4t2iLamqLzvsFxscTT3pbP4utbnDvc6lfvLjM3TLrjupY4Iq5FURmTzhsCstA3sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@modelcontextprotocol/sdk": "^1.26.0",
"@octokit/rest": "^22.0.0",
"commander": "^14.0.2",
"eventsource-parser": "^3.0.6",
"express": "^5.1.0",
@ -810,6 +811,172 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@octokit/auth-token": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/core": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",
"@octokit/request": "^10.0.6",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"before-after-hook": "^4.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/endpoint": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz",
"integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/graphql": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
"integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@octokit/request": "^10.0.6",
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/openapi-types": {
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
"integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
"dev": true,
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
"integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-request-log": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz",
"integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
"integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/request": {
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz",
"integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.2",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"fast-content-type-parse": "^3.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/request-error": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
"integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/rest": {
"version": "22.0.1",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz",
"integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-request-log": "^6.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
"integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^27.0.0"
}
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
@ -1928,6 +2095,13 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/body-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
@ -2752,6 +2926,23 @@
"express": ">= 4.11"
}
},
"node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -4348,6 +4539,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/universal-user-agent": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"dev": true,
"license": "ISC"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View file

@ -123,8 +123,8 @@
},
"devDependencies": {
"@cfworker/json-schema": "^4.1.1",
"@modelcontextprotocol/conformance": "^0.1.11",
"@eslint/js": "^9.39.1",
"@modelcontextprotocol/conformance": "^0.1.14",
"@types/content-type": "^1.1.8",
"@types/cors": "^2.8.17",
"@types/cross-spawn": "^6.0.6",

View file

@ -1,14 +1,8 @@
# Known conformance test failures for v1.x
# These are tracked and should be removed as they're fixed.
#
# tools_call: conformance runner's test server reuses a single Server
# instance across requests, triggering v1.26.0's "Already connected"
# guard (GHSA-345p-7cg4-v4c7). Fixed in conformance repo (PR #141),
# remove this entry once a new conformance release is published.
#
# auth/pre-registration: scenario added in conformance 0.1.11 that
# requires a dedicated client handler for pre-registered credentials.
# Needs to be implemented in both v1.x and main.
# auth/cross-app-access-complete-flow: SEP-990 Enterprise Managed OAuth
# scenario added in conformance 0.1.14. Requires implementing token
# exchange (RFC 8693) and JWT bearer grant (RFC 7523) in the client.
client:
- tools_call
- auth/pre-registration
- auth/cross-app-access-complete-flow

View file

@ -16,6 +16,7 @@ import { ElicitRequestSchema } from '../../../src/types.js';
import { z } from 'zod';
import { logger } from './helpers/logger.js';
import { ConformanceOAuthProvider } from './helpers/conformanceOAuthProvider.js';
import { handle401, withOAuthRetry } from './helpers/withOAuthRetry.js';
/**
@ -37,6 +38,11 @@ const ClientConformanceContextSchema = z.discriminatedUnion('name', [
name: z.literal('auth/client-credentials-basic'),
client_id: z.string(),
client_secret: z.string()
}),
z.object({
name: z.literal('auth/pre-registration'),
client_id: z.string(),
client_secret: z.string()
})
]);
@ -228,6 +234,43 @@ async function runClientCredentialsBasic(serverUrl: string): Promise<void> {
registerScenario('auth/client-credentials-basic', runClientCredentialsBasic);
// ============================================================================
// Pre-registration scenario (no dynamic client registration)
// ============================================================================
async function runPreRegistrationClient(serverUrl: string): Promise<void> {
const ctx = parseContext();
if (ctx.name !== 'auth/pre-registration') {
throw new Error(`Expected pre-registration context, got ${ctx.name}`);
}
// Create a provider pre-populated with registered credentials,
// so the SDK skips dynamic client registration.
const provider = new ConformanceOAuthProvider('http://localhost:3000/callback', {
client_name: 'conformance-pre-registration',
redirect_uris: ['http://localhost:3000/callback']
});
provider.saveClientInformation({
client_id: ctx.client_id,
client_secret: ctx.client_secret,
redirect_uris: ['http://localhost:3000/callback']
});
const oauthFetch = withOAuthRetry('conformance-pre-registration', new URL(serverUrl), handle401, undefined, provider)(fetch);
const client = new Client({ name: 'conformance-pre-registration', version: '1.0.0' }, { capabilities: {} });
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
fetch: oauthFetch
});
await client.connect(transport);
await client.listTools();
await client.callTool({ name: 'test-tool', arguments: {} });
await transport.close();
}
registerScenario('auth/pre-registration', runPreRegistrationClient);
// ============================================================================
// Elicitation defaults scenario
// ============================================================================

View file

@ -38,16 +38,19 @@ export const withOAuthRetry = (
clientName: string,
baseUrl?: string | URL,
handle401Fn: typeof handle401 = handle401,
clientMetadataUrl?: string
clientMetadataUrl?: string,
existingProvider?: ConformanceOAuthProvider
): Middleware => {
const provider = new ConformanceOAuthProvider(
'http://localhost:3000/callback',
{
client_name: clientName,
redirect_uris: ['http://localhost:3000/callback']
},
clientMetadataUrl
);
const provider =
existingProvider ??
new ConformanceOAuthProvider(
'http://localhost:3000/callback',
{
client_name: clientName,
redirect_uris: ['http://localhost:3000/callback']
},
clientMetadataUrl
);
return (next: FetchLike) => {
return async (input: string | URL, init?: RequestInit): Promise<Response> => {
const makeRequest = async (): Promise<Response> => {