feat: implement auth/pre-registration conformance scenario (#1545)
This commit is contained in:
parent
8cbc658483
commit
f2d21458cc
6 changed files with 265 additions and 27 deletions
4
.github/workflows/conformance.yml
vendored
4
.github/workflows/conformance.yml
vendored
|
|
@ -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
208
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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> => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue