Skip to main content
Glama

MCP SSE Ory Server

Official
by ory
oryProvider.ts6.86 kB
import { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients.js'; import { ServerError } from '@modelcontextprotocol/sdk/server/auth/errors.js'; import { AuthorizationParams, OAuthServerProvider, } from '@modelcontextprotocol/sdk/server/auth/provider.js'; import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; import { OAuthClientInformationFull, OAuthClientInformationFullSchema, OAuthTokenRevocationRequest, OAuthTokensSchema, OAuthTokens, } from '@modelcontextprotocol/sdk/shared/auth.js'; import { Response } from 'express'; import * as crypto from 'crypto'; export type OryEndpoints = { authorizationUrl: string; tokenUrl: string; revocationUrl?: string; registrationUrl?: string; }; export type OryOptions = { endpoints: OryEndpoints; verifyAccessToken: (token: string) => Promise<AuthInfo>; getClient: ( clientId: string ) => Promise<OAuthClientInformationFull | undefined>; }; export class OryProvider implements OAuthServerProvider { protected readonly _endpoints: OryEndpoints; protected readonly _verifyAccessToken: (token: string) => Promise<AuthInfo>; protected readonly _getClient: ( clientId: string ) => Promise<OAuthClientInformationFull | undefined>; skipLocalPasswordGrant = false; skipLocalPkceValidation = true; revokeToken?: ( client: OAuthClientInformationFull, request: OAuthTokenRevocationRequest ) => Promise<void>; constructor(options: OryOptions) { this._endpoints = options.endpoints; this._verifyAccessToken = options.verifyAccessToken; this._getClient = options.getClient; if (options.endpoints?.revocationUrl) { this.revokeToken = async ( client: OAuthClientInformationFull, request: OAuthTokenRevocationRequest ) => { const revocationUrl = this._endpoints.revocationUrl; if (!revocationUrl) { throw new Error('No revocation endpoint configured'); } const params = new URLSearchParams(); params.set('token', request.token); params.set('client_id', client.client_id); if (client.client_secret) { params.set('client_secret', client.client_secret); } if (request.token_type_hint) { params.set('token_type_hint', request.token_type_hint); } const response = await fetch(revocationUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); if (!response.ok) { throw new ServerError(`Token revocation failed: ${response.status}`); } }; } } get clientsStore(): OAuthRegisteredClientsStore { const registrationUrl = this._endpoints.registrationUrl; return { getClient: this._getClient, ...(registrationUrl && { registerClient: async (client: OAuthClientInformationFull) => { const response = await fetch(registrationUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(client), }); if (!response.ok) { throw new ServerError( `Client registration failed: ${response.status}` ); } const data = await response.json(); return OAuthClientInformationFullSchema.parse(data); }, }), }; } async authorize( client: OAuthClientInformationFull, params: AuthorizationParams, res: Response ): Promise<void> { let state = ''; if (!params.state) { state = crypto.randomBytes(32).toString('hex'); params.state = state; } if (params.scopes?.length === 0) { params.scopes = ['ory.admin']; } // Start with required OAuth parameters const targetUrl = new URL(this._endpoints.authorizationUrl); const searchParams = new URLSearchParams({ client_id: client.client_id, response_type: 'code', redirect_uri: params.redirectUri, code_challenge: params.codeChallenge, code_challenge_method: 'S256', }); // Add optional standard OAuth parameters if (params.state) searchParams.set('state', params.state); if (params.scopes?.length) searchParams.set('scope', params.scopes.join(' ')); targetUrl.search = searchParams.toString(); res.redirect(targetUrl.toString()); } async challengeForAuthorizationCode( // eslint-disable-next-line @typescript-eslint/no-unused-vars _client: OAuthClientInformationFull, // eslint-disable-next-line @typescript-eslint/no-unused-vars _authorizationCode: string ): Promise<string> { // In a proxy setup, we don't store the code challenge ourselves // Instead, we proxy the token request and let the upstream server validate it return ''; } async exchangeAuthorizationCode( client: OAuthClientInformationFull, authorizationCode: string, codeVerifier?: string ): Promise<OAuthTokens> { const params = new URLSearchParams({ grant_type: 'authorization_code', client_id: client.client_id, code: authorizationCode, redirect_uri: client.redirect_uris[0], }); if (client.client_secret) { params.append('client_secret', client.client_secret); } if (codeVerifier) { params.append('code_verifier', codeVerifier); } const response = await fetch(this._endpoints.tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); if (!response.ok) { throw new ServerError(`Token exchange failed: ${response.status}`); } const data = await response.json(); return OAuthTokensSchema.parse(data); } async exchangeRefreshToken( client: OAuthClientInformationFull, refreshToken: string, scopes?: string[] ): Promise<OAuthTokens> { const params = new URLSearchParams({ grant_type: 'refresh_token', client_id: client.client_id, refresh_token: refreshToken, }); if (client.client_secret) { params.set('client_secret', client.client_secret); } if (scopes?.length) { params.set('scope', scopes.join(' ')); } const response = await fetch(this._endpoints.tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); if (!response.ok) { throw new ServerError(`Token refresh failed: ${response.status}`); } const data = await response.json(); return OAuthTokensSchema.parse(data); } async verifyAccessToken(token: string): Promise<AuthInfo> { return this._verifyAccessToken(token); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ory/mcp-sse'

If you have feedback or need assistance with the MCP directory API, please join our Discord server