| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
- import type {
- OAuthClientMetadata,
- OAuthTokens,
- OAuthClientInformation,
- OAuthClientInformationFull,
- } from "@modelcontextprotocol/sdk/shared/auth.js"
- import { McpAuth } from "./auth"
- import { Log } from "../util/log"
- const log = Log.create({ service: "mcp.oauth" })
- const OAUTH_CALLBACK_PORT = 19876
- const OAUTH_CALLBACK_PATH = "/mcp/oauth/callback"
- export interface McpOAuthConfig {
- clientId?: string
- clientSecret?: string
- scope?: string
- }
- export interface McpOAuthCallbacks {
- onRedirect: (url: URL) => void | Promise<void>
- }
- export class McpOAuthProvider implements OAuthClientProvider {
- constructor(
- private mcpName: string,
- private serverUrl: string,
- private config: McpOAuthConfig,
- private callbacks: McpOAuthCallbacks,
- ) {}
- get redirectUrl(): string {
- return `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`
- }
- get clientMetadata(): OAuthClientMetadata {
- return {
- redirect_uris: [this.redirectUrl],
- client_name: "OpenCode",
- client_uri: "https://opencode.ai",
- grant_types: ["authorization_code", "refresh_token"],
- response_types: ["code"],
- token_endpoint_auth_method: this.config.clientSecret ? "client_secret_post" : "none",
- }
- }
- async clientInformation(): Promise<OAuthClientInformation | undefined> {
- // Check config first (pre-registered client)
- if (this.config.clientId) {
- return {
- client_id: this.config.clientId,
- client_secret: this.config.clientSecret,
- }
- }
- // Check stored client info (from dynamic registration)
- // Use getForUrl to validate credentials are for the current server URL
- const entry = await McpAuth.getForUrl(this.mcpName, this.serverUrl)
- if (entry?.clientInfo) {
- // Check if client secret has expired
- if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
- log.info("client secret expired, need to re-register", { mcpName: this.mcpName })
- return undefined
- }
- return {
- client_id: entry.clientInfo.clientId,
- client_secret: entry.clientInfo.clientSecret,
- }
- }
- // No client info or URL changed - will trigger dynamic registration
- return undefined
- }
- async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
- await McpAuth.updateClientInfo(
- this.mcpName,
- {
- clientId: info.client_id,
- clientSecret: info.client_secret,
- clientIdIssuedAt: info.client_id_issued_at,
- clientSecretExpiresAt: info.client_secret_expires_at,
- },
- this.serverUrl,
- )
- log.info("saved dynamically registered client", {
- mcpName: this.mcpName,
- clientId: info.client_id,
- })
- }
- async tokens(): Promise<OAuthTokens | undefined> {
- // Use getForUrl to validate tokens are for the current server URL
- const entry = await McpAuth.getForUrl(this.mcpName, this.serverUrl)
- if (!entry?.tokens) return undefined
- return {
- access_token: entry.tokens.accessToken,
- token_type: "Bearer",
- refresh_token: entry.tokens.refreshToken,
- expires_in: entry.tokens.expiresAt
- ? Math.max(0, Math.floor(entry.tokens.expiresAt - Date.now() / 1000))
- : undefined,
- scope: entry.tokens.scope,
- }
- }
- async saveTokens(tokens: OAuthTokens): Promise<void> {
- await McpAuth.updateTokens(
- this.mcpName,
- {
- accessToken: tokens.access_token,
- refreshToken: tokens.refresh_token,
- expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
- scope: tokens.scope,
- },
- this.serverUrl,
- )
- log.info("saved oauth tokens", { mcpName: this.mcpName })
- }
- async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
- log.info("redirecting to authorization", { mcpName: this.mcpName, url: authorizationUrl.toString() })
- await this.callbacks.onRedirect(authorizationUrl)
- }
- async saveCodeVerifier(codeVerifier: string): Promise<void> {
- await McpAuth.updateCodeVerifier(this.mcpName, codeVerifier)
- }
- async codeVerifier(): Promise<string> {
- const entry = await McpAuth.get(this.mcpName)
- if (!entry?.codeVerifier) {
- throw new Error(`No code verifier saved for MCP server: ${this.mcpName}`)
- }
- return entry.codeVerifier
- }
- async saveState(state: string): Promise<void> {
- await McpAuth.updateOAuthState(this.mcpName, state)
- }
- async state(): Promise<string> {
- const entry = await McpAuth.get(this.mcpName)
- if (!entry?.oauthState) {
- throw new Error(`No OAuth state saved for MCP server: ${this.mcpName}`)
- }
- return entry.oauthState
- }
- }
- export { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }
|