Skip to main content
Glama
larksuite

Feishu/Lark OpenAPI MCP

Official
by larksuite
handler.ts5.64 kB
import { Express, Request, Response, NextFunction } from 'express'; import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'; import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js'; import { LarkOIDC2OAuthServerProvider, LarkOAuth2OAuthServerProvider } from '../provider'; import { authStore } from '../store'; import { generatePKCEPair } from '../utils/pkce'; import { logger } from '../../utils/logger'; export interface LarkOAuthClientConfig { port: number; host: string; domain: string; appId: string; appSecret: string; scope?: string[]; } export class LarkAuthHandler { protected readonly options: LarkOAuthClientConfig; protected readonly provider: LarkOIDC2OAuthServerProvider | LarkOAuth2OAuthServerProvider; get callbackUrl() { return `http://${this.options.host}:${this.options.port}/callback`; } get issuerUrl() { return `http://${this.options.host}:${this.options.port}`; } constructor( protected readonly app: Express, options: Partial<LarkOAuthClientConfig>, ) { const { port, host, domain, appId, appSecret } = options; if (!port || !host || !domain || !appId || !appSecret) { throw new Error('[Lark MCP] appId, and appSecret are required'); } this.options = options as LarkOAuthClientConfig; const params = { domain, host, port, appId, appSecret, callbackUrl: this.callbackUrl, }; if (!this.options.scope?.length) { this.provider = new LarkOIDC2OAuthServerProvider(params); } else { this.provider = new LarkOAuth2OAuthServerProvider(params); } } protected async callback(req: Request, res: Response) { const redirectUri = req.query.redirect_uri as string; const finalRedirectUri = new URL(redirectUri); finalRedirectUri.searchParams.set('code', req.query.code as string); finalRedirectUri.searchParams.set('state', req.query.state as string); res.redirect(finalRedirectUri.toString()); if (req.query.state === 'reauthorize') { if (!req.query.code || typeof req.query.code !== 'string') { logger.error(`[LarkAuthHandler] Failed to exchange authorization code: ${req.query.code}`); res.end('error, failed to exchange authorization code, please try again'); return; } const codeVerifier = authStore.getCodeVerifier('reauthorize'); if (!codeVerifier) { logger.error(`[LarkAuthHandler] Code verifier not found`); res.end('error: code_verifier not found, please try again'); return; } await this.provider.exchangeAuthorizationCode( { client_id: 'LOCAL', redirect_uris: [] }, req.query.code, codeVerifier, this.callbackUrl, ); authStore.removeCodeVerifier('reauthorize'); logger.info(`[LarkAuthHandler] callback: Successfully exchanged authorization code`); res.end('success, you can close this page now'); } } setupRoutes = (): void => { logger.info(`[LarkAuthHandler] setupRoutes: issuerUrl: ${this.issuerUrl}`); this.app.use(mcpAuthRouter({ provider: this.provider, issuerUrl: new URL(this.issuerUrl) })); this.app.get('/callback', (req, res) => this.callback(req, res)); }; authenticateRequest(req: Request, res: Response, next: NextFunction): void { requireBearerAuth({ verifier: this.provider, requiredScopes: [] })(req, res, next); } async refreshToken(accessToken: string) { const token = await authStore.getToken(accessToken); if (!token) { logger.error(`[LarkAuthHandler] refreshToken: No local access token found`); throw new Error('No local access token found'); } if (!token.extra?.refreshToken) { logger.error(`[LarkAuthHandler] refreshToken: No refresh token found`); throw new Error('No refresh token found'); } const newToken = await this.provider.exchangeRefreshToken( { client_id: token.clientId, redirect_uris: [this.callbackUrl] }, token.extra?.refreshToken as string, token.scopes, ); logger.info(`[LarkAuthHandler] refreshToken: Successfully refreshed token`); await authStore.removeToken(accessToken); return newToken; } async reAuthorize(accessToken?: string) { if (!accessToken) { logger.error(`[LarkAuthHandler] reAuthorize: Invalid access token, please reconnect the mcp server`); throw new Error('Invalid access token, please reconnect the mcp server'); } const token = await authStore.getToken(accessToken); if (!token) { logger.error(`[LarkAuthHandler] reAuthorize: Invalid access token, please reconnect the mcp server`); throw new Error('Invalid access token, please reconnect the mcp server'); } const { clientId } = token; const { codeVerifier, codeChallenge } = generatePKCEPair(); authStore.storeCodeVerifier('reauthorize', codeVerifier); const authorizeUrl = new URL(`http://${this.options.host}:${this.options.port}/authorize`); authorizeUrl.searchParams.set('client_id', clientId); authorizeUrl.searchParams.set('response_type', 'code'); authorizeUrl.searchParams.set('code_challenge', codeChallenge); authorizeUrl.searchParams.set('code_challenge_method', 'S256'); authorizeUrl.searchParams.set('redirect_uri', this.callbackUrl); authorizeUrl.searchParams.set('state', 'reauthorize'); if (this.options.scope) { authorizeUrl.searchParams.set('scope', this.options.scope.join(' ')); } return { accessToken: '', authorizeUrl: authorizeUrl.toString(), }; } }

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/larksuite/lark-openapi-mcp'

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