@dxheroes/local-mcp-backend 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +52 -0
- package/dist/__tests__/integration/mcp-proxy-auth-http.test.js +283 -0
- package/dist/__tests__/integration/mcp-proxy-auth-http.test.js.map +1 -0
- package/dist/__tests__/integration/oauth-authorize-callback.test.js +122 -0
- package/dist/__tests__/integration/oauth-authorize-callback.test.js.map +1 -0
- package/dist/__tests__/integration/proxy-auth.test.js +171 -110
- package/dist/__tests__/integration/proxy-auth.test.js.map +1 -1
- package/dist/__tests__/unit/auth.guard.test.js +23 -2
- package/dist/__tests__/unit/auth.guard.test.js.map +1 -1
- package/dist/common/filters/all-exceptions.filter.js +6 -0
- package/dist/common/filters/all-exceptions.filter.js.map +1 -1
- package/dist/main.js +63 -2
- package/dist/main.js.map +1 -1
- package/dist/modules/auth/auth.config.js +10 -5
- package/dist/modules/auth/auth.config.js.map +1 -1
- package/dist/modules/auth/auth.module.js +5 -2
- package/dist/modules/auth/auth.module.js.map +1 -1
- package/dist/modules/auth/auth.service.js +2 -2
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/mcp-oauth.guard.js +95 -0
- package/dist/modules/auth/mcp-oauth.guard.js.map +1 -0
- package/dist/modules/auth/mcp-oauth.utils.js +75 -0
- package/dist/modules/auth/mcp-oauth.utils.js.map +1 -0
- package/dist/modules/health/health.controller.js +1 -1
- package/dist/modules/health/health.controller.js.map +1 -1
- package/dist/modules/mcp/mcp.service.js +48 -8
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/oauth/oauth.controller.js +78 -1
- package/dist/modules/oauth/oauth.controller.js.map +1 -1
- package/dist/modules/oauth/oauth.service.js +197 -1
- package/dist/modules/oauth/oauth.service.js.map +1 -1
- package/dist/modules/proxy/proxy.controller.js +152 -27
- package/dist/modules/proxy/proxy.controller.js.map +1 -1
- package/dist/modules/proxy/proxy.service.js +28 -4
- package/dist/modules/proxy/proxy.service.js.map +1 -1
- package/docker-entrypoint.sh +15 -2
- package/package.json +9 -7
- package/src/__tests__/integration/mcp-proxy-auth-http.test.ts +311 -0
- package/src/__tests__/integration/oauth-authorize-callback.test.ts +155 -0
- package/src/__tests__/integration/proxy-auth.test.ts +151 -168
- package/src/__tests__/unit/auth.guard.test.ts +12 -2
- package/src/common/filters/all-exceptions.filter.ts +11 -0
- package/src/main.ts +56 -2
- package/src/modules/auth/auth.config.ts +9 -4
- package/src/modules/auth/auth.module.ts +3 -2
- package/src/modules/auth/auth.service.ts +2 -2
- package/src/modules/auth/mcp-oauth.guard.ts +102 -0
- package/src/modules/auth/mcp-oauth.utils.ts +80 -0
- package/src/modules/health/health.controller.ts +1 -1
- package/src/modules/mcp/mcp.service.ts +54 -12
- package/src/modules/oauth/oauth.controller.ts +84 -1
- package/src/modules/oauth/oauth.service.ts +218 -1
- package/src/modules/proxy/proxy.controller.ts +120 -25
- package/src/modules/proxy/proxy.service.ts +26 -4
- package/vitest.config.ts +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/oauth/oauth.service.ts"],"sourcesContent":["/**\n * OAuth Service\n *\n * Manages OAuth tokens for MCP servers.\n */\n\nimport { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';\nimport { PrismaService } from '../database/prisma.service.js';\n\ninterface StartOAuthFlowDto {\n mcpServerId: string;\n authorizationServerUrl: string;\n clientId: string;\n scopes?: string[];\n redirectUri: string;\n}\n\ninterface CompleteOAuthFlowDto {\n mcpServerId: string;\n code: string;\n codeVerifier: string;\n tokenEndpoint: string;\n clientId: string;\n redirectUri: string;\n}\n\ninterface StoreTokenDto {\n mcpServerId: string;\n accessToken: string;\n refreshToken?: string | null;\n tokenType?: string;\n scope?: string | null;\n expiresAt?: Date | null;\n}\n\n@Injectable()\nexport class OAuthService {\n // In-memory storage for PKCE verifiers (in production, use Redis or similar)\n private pkceVerifiers = new Map<string, { verifier: string; expiresAt: number }>();\n\n constructor(private readonly prisma: PrismaService) {}\n\n /**\n * Generate PKCE challenge and store verifier\n */\n async startOAuthFlow(dto: StartOAuthFlowDto) {\n // Check server exists\n const server = await this.prisma.mcpServer.findUnique({ where: { id: dto.mcpServerId } });\n if (!server) {\n throw new NotFoundException(`MCP server ${dto.mcpServerId} not found`);\n }\n\n // Generate PKCE code verifier and challenge\n const codeVerifier = this.generateCodeVerifier();\n const codeChallenge = await this.generateCodeChallenge(codeVerifier);\n\n // Store verifier temporarily (expires in 10 minutes)\n const expiresAt = Date.now() + 10 * 60 * 1000;\n this.pkceVerifiers.set(dto.mcpServerId, { verifier: codeVerifier, expiresAt });\n\n // Build authorization URL\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: dto.clientId,\n redirect_uri: dto.redirectUri,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n });\n\n if (dto.scopes?.length) {\n params.set('scope', dto.scopes.join(' '));\n }\n\n const authorizationUrl = `${dto.authorizationServerUrl}?${params.toString()}`;\n\n return {\n authorizationUrl,\n codeVerifier, // Return for client-side storage if needed\n };\n }\n\n /**\n * Exchange authorization code for tokens\n */\n async completeOAuthFlow(dto: CompleteOAuthFlowDto) {\n // Check server exists\n const server = await this.prisma.mcpServer.findUnique({ where: { id: dto.mcpServerId } });\n if (!server) {\n throw new NotFoundException(`MCP server ${dto.mcpServerId} not found`);\n }\n\n // Exchange code for token\n const response = await fetch(dto.tokenEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: dto.code,\n redirect_uri: dto.redirectUri,\n client_id: dto.clientId,\n code_verifier: dto.codeVerifier,\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new BadRequestException(`Token exchange failed: ${error}`);\n }\n\n const tokenData = (await response.json()) as {\n access_token: string;\n refresh_token?: string;\n token_type?: string;\n scope?: string;\n expires_in?: number;\n };\n\n // Calculate expiry\n const expiresAt = tokenData.expires_in\n ? new Date(Date.now() + tokenData.expires_in * 1000)\n : null;\n\n // Store token\n return this.storeToken({\n mcpServerId: dto.mcpServerId,\n accessToken: tokenData.access_token,\n refreshToken: tokenData.refresh_token,\n tokenType: tokenData.token_type || 'Bearer',\n scope: tokenData.scope,\n expiresAt,\n });\n }\n\n /**\n * Store OAuth token for an MCP server\n */\n async storeToken(dto: StoreTokenDto) {\n return this.prisma.oAuthToken.upsert({\n where: { mcpServerId: dto.mcpServerId },\n update: {\n accessToken: dto.accessToken,\n refreshToken: dto.refreshToken,\n tokenType: dto.tokenType || 'Bearer',\n scope: dto.scope,\n expiresAt: dto.expiresAt,\n },\n create: {\n mcpServerId: dto.mcpServerId,\n accessToken: dto.accessToken,\n refreshToken: dto.refreshToken,\n tokenType: dto.tokenType || 'Bearer',\n scope: dto.scope,\n expiresAt: dto.expiresAt,\n },\n });\n }\n\n /**\n * Get OAuth token for an MCP server\n */\n async getToken(mcpServerId: string) {\n return this.prisma.oAuthToken.findUnique({\n where: { mcpServerId },\n });\n }\n\n /**\n * Delete OAuth token for an MCP server\n */\n async deleteToken(mcpServerId: string) {\n const token = await this.prisma.oAuthToken.findUnique({\n where: { mcpServerId },\n });\n\n if (!token) {\n throw new NotFoundException('OAuth token not found');\n }\n\n await this.prisma.oAuthToken.delete({\n where: { mcpServerId },\n });\n }\n\n /**\n * Generate a random code verifier for PKCE\n */\n private generateCodeVerifier(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return this.base64UrlEncode(array);\n }\n\n /**\n * Generate code challenge from verifier using SHA-256\n */\n private async generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return this.base64UrlEncode(new Uint8Array(hash));\n }\n\n /**\n * Base64 URL encode\n */\n private base64UrlEncode(buffer: Uint8Array): string {\n const base64 = Buffer.from(buffer).toString('base64');\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n }\n}\n"],"names":["BadRequestException","Injectable","NotFoundException","PrismaService","OAuthService","prisma","pkceVerifiers","Map","startOAuthFlow","dto","server","mcpServer","findUnique","where","id","mcpServerId","codeVerifier","generateCodeVerifier","codeChallenge","generateCodeChallenge","expiresAt","Date","now","set","verifier","params","URLSearchParams","response_type","client_id","clientId","redirect_uri","redirectUri","code_challenge","code_challenge_method","scopes","length","join","authorizationUrl","authorizationServerUrl","toString","completeOAuthFlow","response","fetch","tokenEndpoint","method","headers","body","grant_type","code","code_verifier","ok","error","text","tokenData","json","expires_in","storeToken","accessToken","access_token","refreshToken","refresh_token","tokenType","token_type","scope","oAuthToken","upsert","update","create","getToken","deleteToken","token","delete","array","Uint8Array","crypto","getRandomValues","base64UrlEncode","encoder","TextEncoder","data","encode","hash","subtle","digest","buffer","base64","Buffer","from","replace"],"mappings":";;;;;;;;;AAAA;;;;CAIC,GAED,SAASA,mBAAmB,EAAEC,UAAU,EAAEC,iBAAiB,QAAQ,iBAAiB;AACpF,SAASC,aAAa,QAAQ,gCAAgC;AA6B9D,OAAO,MAAMC;IAIX,YAAY,AAAiBC,MAAqB,CAAE;aAAvBA,SAAAA;QAH7B,6EAA6E;aACrEC,gBAAgB,IAAIC;IAEyB;IAErD;;GAEC,GACD,MAAMC,eAAeC,GAAsB,EAAE;QAC3C,sBAAsB;QACtB,MAAMC,SAAS,MAAM,IAAI,CAACL,MAAM,CAACM,SAAS,CAACC,UAAU,CAAC;YAAEC,OAAO;gBAAEC,IAAIL,IAAIM,WAAW;YAAC;QAAE;QACvF,IAAI,CAACL,QAAQ;YACX,MAAM,IAAIR,kBAAkB,CAAC,WAAW,EAAEO,IAAIM,WAAW,CAAC,UAAU,CAAC;QACvE;QAEA,4CAA4C;QAC5C,MAAMC,eAAe,IAAI,CAACC,oBAAoB;QAC9C,MAAMC,gBAAgB,MAAM,IAAI,CAACC,qBAAqB,CAACH;QAEvD,qDAAqD;QACrD,MAAMI,YAAYC,KAAKC,GAAG,KAAK,KAAK,KAAK;QACzC,IAAI,CAAChB,aAAa,CAACiB,GAAG,CAACd,IAAIM,WAAW,EAAE;YAAES,UAAUR;YAAcI;QAAU;QAE5E,0BAA0B;QAC1B,MAAMK,SAAS,IAAIC,gBAAgB;YACjCC,eAAe;YACfC,WAAWnB,IAAIoB,QAAQ;YACvBC,cAAcrB,IAAIsB,WAAW;YAC7BC,gBAAgBd;YAChBe,uBAAuB;QACzB;QAEA,IAAIxB,IAAIyB,MAAM,EAAEC,QAAQ;YACtBV,OAAOF,GAAG,CAAC,SAASd,IAAIyB,MAAM,CAACE,IAAI,CAAC;QACtC;QAEA,MAAMC,mBAAmB,GAAG5B,IAAI6B,sBAAsB,CAAC,CAAC,EAAEb,OAAOc,QAAQ,IAAI;QAE7E,OAAO;YACLF;YACArB;QACF;IACF;IAEA;;GAEC,GACD,MAAMwB,kBAAkB/B,GAAyB,EAAE;QACjD,sBAAsB;QACtB,MAAMC,SAAS,MAAM,IAAI,CAACL,MAAM,CAACM,SAAS,CAACC,UAAU,CAAC;YAAEC,OAAO;gBAAEC,IAAIL,IAAIM,WAAW;YAAC;QAAE;QACvF,IAAI,CAACL,QAAQ;YACX,MAAM,IAAIR,kBAAkB,CAAC,WAAW,EAAEO,IAAIM,WAAW,CAAC,UAAU,CAAC;QACvE;QAEA,0BAA0B;QAC1B,MAAM0B,WAAW,MAAMC,MAAMjC,IAAIkC,aAAa,EAAE;YAC9CC,QAAQ;YACRC,SAAS;gBACP,gBAAgB;YAClB;YACAC,MAAM,IAAIpB,gBAAgB;gBACxBqB,YAAY;gBACZC,MAAMvC,IAAIuC,IAAI;gBACdlB,cAAcrB,IAAIsB,WAAW;gBAC7BH,WAAWnB,IAAIoB,QAAQ;gBACvBoB,eAAexC,IAAIO,YAAY;YACjC;QACF;QAEA,IAAI,CAACyB,SAASS,EAAE,EAAE;YAChB,MAAMC,QAAQ,MAAMV,SAASW,IAAI;YACjC,MAAM,IAAIpD,oBAAoB,CAAC,uBAAuB,EAAEmD,OAAO;QACjE;QAEA,MAAME,YAAa,MAAMZ,SAASa,IAAI;QAQtC,mBAAmB;QACnB,MAAMlC,YAAYiC,UAAUE,UAAU,GAClC,IAAIlC,KAAKA,KAAKC,GAAG,KAAK+B,UAAUE,UAAU,GAAG,QAC7C;QAEJ,cAAc;QACd,OAAO,IAAI,CAACC,UAAU,CAAC;YACrBzC,aAAaN,IAAIM,WAAW;YAC5B0C,aAAaJ,UAAUK,YAAY;YACnCC,cAAcN,UAAUO,aAAa;YACrCC,WAAWR,UAAUS,UAAU,IAAI;YACnCC,OAAOV,UAAUU,KAAK;YACtB3C;QACF;IACF;IAEA;;GAEC,GACD,MAAMoC,WAAW/C,GAAkB,EAAE;QACnC,OAAO,IAAI,CAACJ,MAAM,CAAC2D,UAAU,CAACC,MAAM,CAAC;YACnCpD,OAAO;gBAAEE,aAAaN,IAAIM,WAAW;YAAC;YACtCmD,QAAQ;gBACNT,aAAahD,IAAIgD,WAAW;gBAC5BE,cAAclD,IAAIkD,YAAY;gBAC9BE,WAAWpD,IAAIoD,SAAS,IAAI;gBAC5BE,OAAOtD,IAAIsD,KAAK;gBAChB3C,WAAWX,IAAIW,SAAS;YAC1B;YACA+C,QAAQ;gBACNpD,aAAaN,IAAIM,WAAW;gBAC5B0C,aAAahD,IAAIgD,WAAW;gBAC5BE,cAAclD,IAAIkD,YAAY;gBAC9BE,WAAWpD,IAAIoD,SAAS,IAAI;gBAC5BE,OAAOtD,IAAIsD,KAAK;gBAChB3C,WAAWX,IAAIW,SAAS;YAC1B;QACF;IACF;IAEA;;GAEC,GACD,MAAMgD,SAASrD,WAAmB,EAAE;QAClC,OAAO,IAAI,CAACV,MAAM,CAAC2D,UAAU,CAACpD,UAAU,CAAC;YACvCC,OAAO;gBAAEE;YAAY;QACvB;IACF;IAEA;;GAEC,GACD,MAAMsD,YAAYtD,WAAmB,EAAE;QACrC,MAAMuD,QAAQ,MAAM,IAAI,CAACjE,MAAM,CAAC2D,UAAU,CAACpD,UAAU,CAAC;YACpDC,OAAO;gBAAEE;YAAY;QACvB;QAEA,IAAI,CAACuD,OAAO;YACV,MAAM,IAAIpE,kBAAkB;QAC9B;QAEA,MAAM,IAAI,CAACG,MAAM,CAAC2D,UAAU,CAACO,MAAM,CAAC;YAClC1D,OAAO;gBAAEE;YAAY;QACvB;IACF;IAEA;;GAEC,GACD,AAAQE,uBAA+B;QACrC,MAAMuD,QAAQ,IAAIC,WAAW;QAC7BC,OAAOC,eAAe,CAACH;QACvB,OAAO,IAAI,CAACI,eAAe,CAACJ;IAC9B;IAEA;;GAEC,GACD,MAAcrD,sBAAsBK,QAAgB,EAAmB;QACrE,MAAMqD,UAAU,IAAIC;QACpB,MAAMC,OAAOF,QAAQG,MAAM,CAACxD;QAC5B,MAAMyD,OAAO,MAAMP,OAAOQ,MAAM,CAACC,MAAM,CAAC,WAAWJ;QACnD,OAAO,IAAI,CAACH,eAAe,CAAC,IAAIH,WAAWQ;IAC7C;IAEA;;GAEC,GACD,AAAQL,gBAAgBQ,MAAkB,EAAU;QAClD,MAAMC,SAASC,OAAOC,IAAI,CAACH,QAAQ7C,QAAQ,CAAC;QAC5C,OAAO8C,OAAOG,OAAO,CAAC,OAAO,KAAKA,OAAO,CAAC,OAAO,KAAKA,OAAO,CAAC,OAAO;IACvE;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/oauth/oauth.service.ts"],"sourcesContent":["/**\n * OAuth Service\n *\n * Manages OAuth tokens for MCP servers.\n */\n\nimport { OAuthDiscoveryService } from '@dxheroes/local-mcp-core';\nimport { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';\nimport { PrismaService } from '../database/prisma.service.js';\n\ninterface StartOAuthFlowDto {\n mcpServerId: string;\n authorizationServerUrl: string;\n clientId: string;\n scopes?: string[];\n redirectUri: string;\n}\n\ninterface CompleteOAuthFlowDto {\n mcpServerId: string;\n code: string;\n codeVerifier: string;\n tokenEndpoint: string;\n clientId: string;\n redirectUri: string;\n}\n\ninterface StoreTokenDto {\n mcpServerId: string;\n accessToken: string;\n refreshToken?: string | null;\n tokenType?: string;\n scope?: string | null;\n expiresAt?: Date | null;\n}\n\n@Injectable()\nexport class OAuthService {\n private readonly logger = new Logger(OAuthService.name);\n // In-memory storage for PKCE verifiers (in production, use Redis or similar)\n private pkceVerifiers = new Map<string, { verifier: string; expiresAt: number }>();\n private readonly discoveryService = new OAuthDiscoveryService();\n\n constructor(private readonly prisma: PrismaService) {}\n\n /**\n * Generate PKCE challenge and store verifier\n */\n async startOAuthFlow(dto: StartOAuthFlowDto) {\n // Check server exists\n const server = await this.prisma.mcpServer.findUnique({ where: { id: dto.mcpServerId } });\n if (!server) {\n throw new NotFoundException(`MCP server ${dto.mcpServerId} not found`);\n }\n\n // Generate PKCE code verifier and challenge\n const codeVerifier = this.generateCodeVerifier();\n const codeChallenge = await this.generateCodeChallenge(codeVerifier);\n\n // Store verifier temporarily (expires in 10 minutes)\n const expiresAt = Date.now() + 10 * 60 * 1000;\n this.pkceVerifiers.set(dto.mcpServerId, { verifier: codeVerifier, expiresAt });\n\n // Build authorization URL\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: dto.clientId,\n redirect_uri: dto.redirectUri,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n });\n\n if (dto.scopes?.length) {\n params.set('scope', dto.scopes.join(' '));\n }\n\n const authorizationUrl = `${dto.authorizationServerUrl}?${params.toString()}`;\n\n return {\n authorizationUrl,\n codeVerifier, // Return for client-side storage if needed\n };\n }\n\n /**\n * Exchange authorization code for tokens\n */\n async completeOAuthFlow(dto: CompleteOAuthFlowDto) {\n // Check server exists\n const server = await this.prisma.mcpServer.findUnique({ where: { id: dto.mcpServerId } });\n if (!server) {\n throw new NotFoundException(`MCP server ${dto.mcpServerId} not found`);\n }\n\n // Exchange code for token\n const response = await fetch(dto.tokenEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: dto.code,\n redirect_uri: dto.redirectUri,\n client_id: dto.clientId,\n code_verifier: dto.codeVerifier,\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new BadRequestException(`Token exchange failed: ${error}`);\n }\n\n const tokenData = (await response.json()) as {\n access_token: string;\n refresh_token?: string;\n token_type?: string;\n scope?: string;\n expires_in?: number;\n };\n\n // Calculate expiry\n const expiresAt = tokenData.expires_in\n ? new Date(Date.now() + tokenData.expires_in * 1000)\n : null;\n\n // Store token\n return this.storeToken({\n mcpServerId: dto.mcpServerId,\n accessToken: tokenData.access_token,\n refreshToken: tokenData.refresh_token,\n tokenType: tokenData.token_type || 'Bearer',\n scope: tokenData.scope,\n expiresAt,\n });\n }\n\n /**\n * Store OAuth token for an MCP server\n */\n async storeToken(dto: StoreTokenDto) {\n return this.prisma.oAuthToken.upsert({\n where: { mcpServerId: dto.mcpServerId },\n update: {\n accessToken: dto.accessToken,\n refreshToken: dto.refreshToken,\n tokenType: dto.tokenType || 'Bearer',\n scope: dto.scope,\n expiresAt: dto.expiresAt,\n },\n create: {\n mcpServerId: dto.mcpServerId,\n accessToken: dto.accessToken,\n refreshToken: dto.refreshToken,\n tokenType: dto.tokenType || 'Bearer',\n scope: dto.scope,\n expiresAt: dto.expiresAt,\n },\n });\n }\n\n /**\n * Get OAuth token for an MCP server\n */\n async getToken(mcpServerId: string) {\n return this.prisma.oAuthToken.findUnique({\n where: { mcpServerId },\n });\n }\n\n /**\n * Delete OAuth token for an MCP server\n */\n async deleteToken(mcpServerId: string) {\n const token = await this.prisma.oAuthToken.findUnique({\n where: { mcpServerId },\n });\n\n if (!token) {\n throw new NotFoundException('OAuth token not found');\n }\n\n await this.prisma.oAuthToken.delete({\n where: { mcpServerId },\n });\n }\n\n /**\n * Discover OAuth configuration and build an authorization URL for a server.\n * Uses RFC 9728 / 8414 / 7591 auto-discovery and optional DCR.\n * Returns an authorization URL the browser should be redirected to.\n */\n async discoverAndAuthorize(serverId: string, callbackUrl: string): Promise<string> {\n const server = await this.prisma.mcpServer.findUnique({ where: { id: serverId } });\n if (!server) {\n throw new NotFoundException(`MCP server ${serverId} not found`);\n }\n\n // Parse server URL from config\n const config = typeof server.config === 'string' ? JSON.parse(server.config) : server.config;\n const serverUrl = config?.url as string | undefined;\n if (!serverUrl) {\n throw new BadRequestException('Server has no URL configured');\n }\n\n // Parse existing oauthConfig if any (may have manual clientId)\n const existingOAuthConfig = server.oauthConfig\n ? typeof server.oauthConfig === 'string'\n ? JSON.parse(server.oauthConfig)\n : server.oauthConfig\n : null;\n\n // Step 1: Discover OAuth endpoints via RFC 9728 + 8414\n this.logger.log(`Discovering OAuth for server ${serverId} at ${serverUrl}`);\n const discovery = await this.discoveryService.discoverFromServerUrl(serverUrl);\n\n // Step 2: Get or register client\n let clientId: string;\n\n if (existingOAuthConfig?.clientId) {\n // Use manually configured clientId\n clientId = existingOAuthConfig.clientId;\n } else {\n // Check for existing DCR registration\n const existingReg = await this.prisma.oAuthClientRegistration.findUnique({\n where: {\n mcpServerId_authorizationServerUrl: {\n mcpServerId: serverId,\n authorizationServerUrl: discovery.authorizationServerUrl,\n },\n },\n });\n\n if (existingReg) {\n // Delete stale registration — redirect_uri may have changed\n await this.prisma.oAuthClientRegistration.delete({\n where: { id: existingReg.id },\n });\n this.logger.log(`Deleted stale DCR registration for ${serverId}, will re-register`);\n }\n\n if (discovery.registrationEndpoint) {\n // Perform Dynamic Client Registration (RFC 7591)\n this.logger.log(`Performing DCR at ${discovery.registrationEndpoint}`);\n const registration = await this.discoveryService.registerClient(\n discovery.registrationEndpoint,\n callbackUrl,\n discovery.scopes\n );\n clientId = registration.clientId;\n\n // Store registration\n await this.prisma.oAuthClientRegistration.create({\n data: {\n mcpServerId: serverId,\n authorizationServerUrl: discovery.authorizationServerUrl,\n clientId: registration.clientId,\n clientSecret: registration.clientSecret,\n registrationAccessToken: registration.registrationAccessToken,\n },\n });\n } else {\n throw new BadRequestException(\n 'No clientId configured and server does not support Dynamic Client Registration. ' +\n 'Please configure a clientId manually in the OAuth settings.'\n );\n }\n }\n\n // Step 3: Store discovery result in oauthConfig on the server\n await this.prisma.mcpServer.update({\n where: { id: serverId },\n data: {\n oauthConfig: JSON.stringify({\n authorizationServerUrl: discovery.authorizationServerUrl,\n authorizationEndpoint: discovery.authorizationEndpoint,\n tokenEndpoint: discovery.tokenEndpoint,\n registrationEndpoint: discovery.registrationEndpoint,\n resource: discovery.resource,\n scopes: discovery.scopes,\n clientId,\n requiresOAuth: true,\n }),\n },\n });\n\n // Step 4: Generate PKCE\n const codeVerifier = this.generateCodeVerifier();\n const codeChallenge = await this.generateCodeChallenge(codeVerifier);\n\n // Store verifier (expires in 10 minutes)\n this.pkceVerifiers.set(serverId, {\n verifier: codeVerifier,\n expiresAt: Date.now() + 10 * 60 * 1000,\n });\n\n // Step 5: Build authorization URL\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: clientId,\n redirect_uri: callbackUrl,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state: serverId, // Use serverId as state for callback routing\n });\n\n if (discovery.scopes.length > 0) {\n params.set('scope', discovery.scopes.join(' '));\n }\n\n if (discovery.resource) {\n params.set('resource', discovery.resource);\n }\n\n return `${discovery.authorizationEndpoint}?${params.toString()}`;\n }\n\n /**\n * Handle OAuth callback: exchange authorization code for tokens.\n */\n async handleCallback(\n serverId: string,\n code: string,\n callbackUrl: string\n ): Promise<{ success: boolean; error?: string }> {\n const server = await this.prisma.mcpServer.findUnique({ where: { id: serverId } });\n if (!server) {\n return { success: false, error: `MCP server ${serverId} not found` };\n }\n\n // Retrieve PKCE verifier\n const pkceEntry = this.pkceVerifiers.get(serverId);\n if (!pkceEntry) {\n return { success: false, error: 'PKCE verifier not found or expired' };\n }\n if (pkceEntry.expiresAt < Date.now()) {\n this.pkceVerifiers.delete(serverId);\n return { success: false, error: 'PKCE verifier expired' };\n }\n const codeVerifier = pkceEntry.verifier;\n this.pkceVerifiers.delete(serverId);\n\n // Get oauthConfig for token endpoint and clientId\n const oauthConfig = server.oauthConfig\n ? typeof server.oauthConfig === 'string'\n ? JSON.parse(server.oauthConfig)\n : server.oauthConfig\n : null;\n\n if (!oauthConfig?.tokenEndpoint || !oauthConfig?.clientId) {\n return { success: false, error: 'OAuth configuration incomplete (missing tokenEndpoint or clientId)' };\n }\n\n try {\n // Exchange code for tokens\n const response = await fetch(oauthConfig.tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: callbackUrl,\n client_id: oauthConfig.clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n return { success: false, error: `Token exchange failed: ${errorText}` };\n }\n\n const tokenData = (await response.json()) as {\n access_token: string;\n refresh_token?: string;\n token_type?: string;\n scope?: string;\n expires_in?: number;\n };\n\n const expiresAt = tokenData.expires_in\n ? new Date(Date.now() + tokenData.expires_in * 1000)\n : null;\n\n // Store token\n await this.storeToken({\n mcpServerId: serverId,\n accessToken: tokenData.access_token,\n refreshToken: tokenData.refresh_token,\n tokenType: tokenData.token_type || 'Bearer',\n scope: tokenData.scope,\n expiresAt,\n });\n\n return { success: true };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return { success: false, error: message };\n }\n }\n\n /**\n * Generate a random code verifier for PKCE\n */\n private generateCodeVerifier(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return this.base64UrlEncode(array);\n }\n\n /**\n * Generate code challenge from verifier using SHA-256\n */\n private async generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return this.base64UrlEncode(new Uint8Array(hash));\n }\n\n /**\n * Base64 URL encode\n */\n private base64UrlEncode(buffer: Uint8Array): string {\n const base64 = Buffer.from(buffer).toString('base64');\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n }\n}\n"],"names":["OAuthDiscoveryService","BadRequestException","Injectable","Logger","NotFoundException","PrismaService","OAuthService","prisma","logger","name","pkceVerifiers","Map","discoveryService","startOAuthFlow","dto","server","mcpServer","findUnique","where","id","mcpServerId","codeVerifier","generateCodeVerifier","codeChallenge","generateCodeChallenge","expiresAt","Date","now","set","verifier","params","URLSearchParams","response_type","client_id","clientId","redirect_uri","redirectUri","code_challenge","code_challenge_method","scopes","length","join","authorizationUrl","authorizationServerUrl","toString","completeOAuthFlow","response","fetch","tokenEndpoint","method","headers","body","grant_type","code","code_verifier","ok","error","text","tokenData","json","expires_in","storeToken","accessToken","access_token","refreshToken","refresh_token","tokenType","token_type","scope","oAuthToken","upsert","update","create","getToken","deleteToken","token","delete","discoverAndAuthorize","serverId","callbackUrl","config","JSON","parse","serverUrl","url","existingOAuthConfig","oauthConfig","log","discovery","discoverFromServerUrl","existingReg","oAuthClientRegistration","mcpServerId_authorizationServerUrl","registrationEndpoint","registration","registerClient","data","clientSecret","registrationAccessToken","stringify","authorizationEndpoint","resource","requiresOAuth","state","handleCallback","success","pkceEntry","get","errorText","message","Error","array","Uint8Array","crypto","getRandomValues","base64UrlEncode","encoder","TextEncoder","encode","hash","subtle","digest","buffer","base64","Buffer","from","replace"],"mappings":"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"}
|
|
@@ -20,38 +20,24 @@ function _ts_param(paramIndex, decorator) {
|
|
|
20
20
|
* Profile endpoints use org slug: /api/mcp/:orgSlug/:profileName
|
|
21
21
|
*
|
|
22
22
|
* @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http
|
|
23
|
-
*/ import { Body, Controller, Get, HttpCode, HttpStatus, NotFoundException, Param, Post, Req, Res,
|
|
23
|
+
*/ import { Body, Controller, Get, HttpCode, HttpStatus, NotFoundException, Param, Post, Req, Res, UseGuards } from "@nestjs/common";
|
|
24
24
|
import { EventEmitter2 } from "@nestjs/event-emitter";
|
|
25
25
|
import { fromEvent, map } from "rxjs";
|
|
26
|
-
import { AuthService } from "../auth/auth.service.js";
|
|
27
26
|
import { Public } from "../auth/decorators/public.decorator.js";
|
|
27
|
+
import { McpOAuthGuard } from "../auth/mcp-oauth.guard.js";
|
|
28
28
|
import { GATEWAY_PROFILE_CHANGED, SettingsService } from "../settings/settings.service.js";
|
|
29
29
|
import { ProxyService } from "./proxy.service.js";
|
|
30
30
|
export class ProxyController {
|
|
31
|
-
constructor(proxyService, settingsService, eventEmitter
|
|
31
|
+
constructor(proxyService, settingsService, eventEmitter){
|
|
32
32
|
this.proxyService = proxyService;
|
|
33
33
|
this.settingsService = settingsService;
|
|
34
34
|
this.eventEmitter = eventEmitter;
|
|
35
|
-
this.authService = authService;
|
|
36
35
|
}
|
|
37
36
|
/**
|
|
38
|
-
* Resolve user from
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const authHeader = req.headers.authorization;
|
|
43
|
-
if (!authHeader?.startsWith('Bearer ')) {
|
|
44
|
-
// No token — unauthenticated MCP client, can only access system profiles
|
|
45
|
-
return {
|
|
46
|
-
id: '__unauthenticated__'
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
const token = authHeader.slice(7);
|
|
50
|
-
const user = await this.authService.validateMcpToken(token);
|
|
51
|
-
if (!user) {
|
|
52
|
-
throw new UnauthorizedException('Invalid or expired MCP OAuth token');
|
|
53
|
-
}
|
|
54
|
-
return user;
|
|
37
|
+
* Resolve user from request.
|
|
38
|
+
* McpOAuthGuard always validates and attaches user before this is called.
|
|
39
|
+
*/ resolveUser(req) {
|
|
40
|
+
return req.user;
|
|
55
41
|
}
|
|
56
42
|
// =========================================
|
|
57
43
|
// Gateway Endpoints (must come BEFORE parameterized routes)
|
|
@@ -69,7 +55,7 @@ export class ProxyController {
|
|
|
69
55
|
/**
|
|
70
56
|
* Get gateway info
|
|
71
57
|
*/ async getGatewayInfo(req) {
|
|
72
|
-
const user =
|
|
58
|
+
const user = this.resolveUser(req);
|
|
73
59
|
const profileName = await this.settingsService.getDefaultGatewayProfile();
|
|
74
60
|
try {
|
|
75
61
|
const info = await this.proxyService.getProfileInfo(profileName, user.id);
|
|
@@ -93,7 +79,7 @@ export class ProxyController {
|
|
|
93
79
|
if (acceptHeader.includes('text/event-stream')) {
|
|
94
80
|
return this.streamGatewayEvents(req, res);
|
|
95
81
|
}
|
|
96
|
-
const user =
|
|
82
|
+
const user = this.resolveUser(req);
|
|
97
83
|
const profileName = await this.settingsService.getDefaultGatewayProfile();
|
|
98
84
|
try {
|
|
99
85
|
const info = await this.proxyService.getProfileInfo(profileName, user.id);
|
|
@@ -152,7 +138,7 @@ export class ProxyController {
|
|
|
152
138
|
/**
|
|
153
139
|
* MCP JSON-RPC endpoint for the gateway
|
|
154
140
|
*/ async handleGatewayRequest(req, request) {
|
|
155
|
-
const user =
|
|
141
|
+
const user = this.resolveUser(req);
|
|
156
142
|
const profileName = await this.settingsService.getDefaultGatewayProfile();
|
|
157
143
|
try {
|
|
158
144
|
return await this.proxyService.handleRequest(profileName, request, user.id);
|
|
@@ -164,6 +150,82 @@ export class ProxyController {
|
|
|
164
150
|
}
|
|
165
151
|
}
|
|
166
152
|
// =========================================
|
|
153
|
+
// Org-scoped Gateway Endpoints: /api/mcp/:orgSlug/gateway
|
|
154
|
+
// (must come BEFORE :orgSlug/:profileName to avoid "gateway" matching :profileName)
|
|
155
|
+
// =========================================
|
|
156
|
+
/**
|
|
157
|
+
* SSE endpoint for org-scoped gateway
|
|
158
|
+
*/ async streamOrgGatewaySse(orgSlug, req, res) {
|
|
159
|
+
const profileName = await this.settingsService.getDefaultGatewayProfile();
|
|
160
|
+
await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);
|
|
161
|
+
return this.streamGatewayEvents(req, res);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* POST handler for org-scoped gateway SSE
|
|
165
|
+
*/ async handleOrgGatewaySseRequest(req, orgSlug, request) {
|
|
166
|
+
const user = this.resolveUser(req);
|
|
167
|
+
const profileName = await this.settingsService.getDefaultGatewayProfile();
|
|
168
|
+
return this.proxyService.handleRequestByOrgSlug(profileName, orgSlug, request, user.id);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get org-scoped gateway info
|
|
172
|
+
*/ async getOrgGatewayInfo(orgSlug) {
|
|
173
|
+
const profileName = await this.settingsService.getDefaultGatewayProfile();
|
|
174
|
+
const info = await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);
|
|
175
|
+
return {
|
|
176
|
+
...info,
|
|
177
|
+
gateway: {
|
|
178
|
+
activeProfile: profileName
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* GET handler for org-scoped gateway endpoint
|
|
184
|
+
*/ async getOrgGatewayEndpoint(orgSlug, req, res) {
|
|
185
|
+
const acceptHeader = req.headers.accept || '';
|
|
186
|
+
if (acceptHeader.includes('text/event-stream')) {
|
|
187
|
+
const profileName = await this.settingsService.getDefaultGatewayProfile();
|
|
188
|
+
await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);
|
|
189
|
+
return this.streamGatewayEvents(req, res);
|
|
190
|
+
}
|
|
191
|
+
const profileName = await this.settingsService.getDefaultGatewayProfile();
|
|
192
|
+
const info = await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);
|
|
193
|
+
return res.json({
|
|
194
|
+
message: 'This is the MCP Gateway endpoint. Use POST for JSON-RPC requests.',
|
|
195
|
+
usage: {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
contentType: 'application/json',
|
|
198
|
+
body: {
|
|
199
|
+
jsonrpc: '2.0',
|
|
200
|
+
method: 'tools/list',
|
|
201
|
+
id: 1
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
endpoints: {
|
|
205
|
+
sse: `/api/mcp/${orgSlug}/gateway/sse`,
|
|
206
|
+
http: `/api/mcp/${orgSlug}/gateway`
|
|
207
|
+
},
|
|
208
|
+
gateway: {
|
|
209
|
+
activeProfile: profileName,
|
|
210
|
+
settingsEndpoint: '/api/settings/default-gateway-profile'
|
|
211
|
+
},
|
|
212
|
+
profile: {
|
|
213
|
+
name: profileName,
|
|
214
|
+
toolCount: info.tools.length,
|
|
215
|
+
serverCount: info.serverStatus.total,
|
|
216
|
+
connectedServers: info.serverStatus.connected
|
|
217
|
+
},
|
|
218
|
+
infoEndpoint: `/api/mcp/${orgSlug}/gateway/info`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* MCP JSON-RPC endpoint for org-scoped gateway
|
|
223
|
+
*/ async handleOrgGatewayRequest(req, orgSlug, request) {
|
|
224
|
+
const user = this.resolveUser(req);
|
|
225
|
+
const profileName = await this.settingsService.getDefaultGatewayProfile();
|
|
226
|
+
return this.proxyService.handleRequestByOrgSlug(profileName, orgSlug, request, user.id);
|
|
227
|
+
}
|
|
228
|
+
// =========================================
|
|
167
229
|
// Org-scoped Profile Endpoints: /api/mcp/:orgSlug/:profileName
|
|
168
230
|
// =========================================
|
|
169
231
|
/**
|
|
@@ -217,7 +279,7 @@ export class ProxyController {
|
|
|
217
279
|
/**
|
|
218
280
|
* MCP JSON-RPC endpoint for an org-scoped profile
|
|
219
281
|
*/ async handleOrgMcpRequest(req, orgSlug, profileName, request) {
|
|
220
|
-
const user =
|
|
282
|
+
const user = this.resolveUser(req);
|
|
221
283
|
return this.proxyService.handleRequestByOrgSlug(profileName, orgSlug, request, user.id);
|
|
222
284
|
}
|
|
223
285
|
}
|
|
@@ -276,6 +338,69 @@ _ts_decorate([
|
|
|
276
338
|
]),
|
|
277
339
|
_ts_metadata("design:returntype", Promise)
|
|
278
340
|
], ProxyController.prototype, "handleGatewayRequest", null);
|
|
341
|
+
_ts_decorate([
|
|
342
|
+
Get(':orgSlug/gateway/sse'),
|
|
343
|
+
_ts_param(0, Param('orgSlug')),
|
|
344
|
+
_ts_param(1, Req()),
|
|
345
|
+
_ts_param(2, Res()),
|
|
346
|
+
_ts_metadata("design:type", Function),
|
|
347
|
+
_ts_metadata("design:paramtypes", [
|
|
348
|
+
String,
|
|
349
|
+
typeof Request === "undefined" ? Object : Request,
|
|
350
|
+
typeof Response === "undefined" ? Object : Response
|
|
351
|
+
]),
|
|
352
|
+
_ts_metadata("design:returntype", Promise)
|
|
353
|
+
], ProxyController.prototype, "streamOrgGatewaySse", null);
|
|
354
|
+
_ts_decorate([
|
|
355
|
+
Post(':orgSlug/gateway/sse'),
|
|
356
|
+
HttpCode(HttpStatus.OK),
|
|
357
|
+
_ts_param(0, Req()),
|
|
358
|
+
_ts_param(1, Param('orgSlug')),
|
|
359
|
+
_ts_param(2, Body()),
|
|
360
|
+
_ts_metadata("design:type", Function),
|
|
361
|
+
_ts_metadata("design:paramtypes", [
|
|
362
|
+
typeof Request === "undefined" ? Object : Request,
|
|
363
|
+
String,
|
|
364
|
+
typeof McpRequest === "undefined" ? Object : McpRequest
|
|
365
|
+
]),
|
|
366
|
+
_ts_metadata("design:returntype", Promise)
|
|
367
|
+
], ProxyController.prototype, "handleOrgGatewaySseRequest", null);
|
|
368
|
+
_ts_decorate([
|
|
369
|
+
Get(':orgSlug/gateway/info'),
|
|
370
|
+
_ts_param(0, Param('orgSlug')),
|
|
371
|
+
_ts_metadata("design:type", Function),
|
|
372
|
+
_ts_metadata("design:paramtypes", [
|
|
373
|
+
String
|
|
374
|
+
]),
|
|
375
|
+
_ts_metadata("design:returntype", Promise)
|
|
376
|
+
], ProxyController.prototype, "getOrgGatewayInfo", null);
|
|
377
|
+
_ts_decorate([
|
|
378
|
+
Get(':orgSlug/gateway'),
|
|
379
|
+
_ts_param(0, Param('orgSlug')),
|
|
380
|
+
_ts_param(1, Req()),
|
|
381
|
+
_ts_param(2, Res()),
|
|
382
|
+
_ts_metadata("design:type", Function),
|
|
383
|
+
_ts_metadata("design:paramtypes", [
|
|
384
|
+
String,
|
|
385
|
+
typeof Request === "undefined" ? Object : Request,
|
|
386
|
+
typeof Response === "undefined" ? Object : Response
|
|
387
|
+
]),
|
|
388
|
+
_ts_metadata("design:returntype", Promise)
|
|
389
|
+
], ProxyController.prototype, "getOrgGatewayEndpoint", null);
|
|
390
|
+
_ts_decorate([
|
|
391
|
+
Post(':orgSlug/gateway'),
|
|
392
|
+
HttpCode(HttpStatus.OK),
|
|
393
|
+
_ts_param(0, Req()),
|
|
394
|
+
_ts_param(1, Param('orgSlug')),
|
|
395
|
+
_ts_param(2, Body()),
|
|
396
|
+
_ts_metadata("design:type", Function),
|
|
397
|
+
_ts_metadata("design:paramtypes", [
|
|
398
|
+
typeof Request === "undefined" ? Object : Request,
|
|
399
|
+
String,
|
|
400
|
+
typeof McpRequest === "undefined" ? Object : McpRequest
|
|
401
|
+
]),
|
|
402
|
+
_ts_metadata("design:returntype", Promise)
|
|
403
|
+
], ProxyController.prototype, "handleOrgGatewayRequest", null);
|
|
279
404
|
_ts_decorate([
|
|
280
405
|
Get(':orgSlug/:profileName/sse'),
|
|
281
406
|
_ts_param(0, Param('orgSlug')),
|
|
@@ -351,13 +476,13 @@ _ts_decorate([
|
|
|
351
476
|
], ProxyController.prototype, "handleOrgMcpRequest", null);
|
|
352
477
|
ProxyController = _ts_decorate([
|
|
353
478
|
Public(),
|
|
479
|
+
UseGuards(McpOAuthGuard),
|
|
354
480
|
Controller('mcp'),
|
|
355
481
|
_ts_metadata("design:type", Function),
|
|
356
482
|
_ts_metadata("design:paramtypes", [
|
|
357
483
|
typeof ProxyService === "undefined" ? Object : ProxyService,
|
|
358
484
|
typeof SettingsService === "undefined" ? Object : SettingsService,
|
|
359
|
-
typeof EventEmitter2 === "undefined" ? Object : EventEmitter2
|
|
360
|
-
typeof AuthService === "undefined" ? Object : AuthService
|
|
485
|
+
typeof EventEmitter2 === "undefined" ? Object : EventEmitter2
|
|
361
486
|
])
|
|
362
487
|
], ProxyController);
|
|
363
488
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/proxy/proxy.controller.ts"],"sourcesContent":["/**\n * Proxy Controller\n *\n * MCP proxy endpoints for profiles and gateway.\n * Supports MCP Streamable HTTP transport (2025-11-25 spec) with SSE notifications.\n * Profile endpoints use org slug: /api/mcp/:orgSlug/:profileName\n *\n * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http\n */\n\nimport {\n Body,\n Controller,\n Get,\n HttpCode,\n HttpStatus,\n NotFoundException,\n Param,\n Post,\n Req,\n Res,\n UnauthorizedException,\n} from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport type { Request, Response } from 'express';\nimport { fromEvent, map } from 'rxjs';\nimport { AuthService } from '../auth/auth.service.js';\nimport { Public } from '../auth/decorators/public.decorator.js';\nimport { GATEWAY_PROFILE_CHANGED, SettingsService } from '../settings/settings.service.js';\nimport type { McpRequest, McpResponse } from './proxy.service.js';\nimport { ProxyService } from './proxy.service.js';\n\n@Public()\n@Controller('mcp')\nexport class ProxyController {\n constructor(\n private readonly proxyService: ProxyService,\n private readonly settingsService: SettingsService,\n private readonly eventEmitter: EventEmitter2,\n private readonly authService: AuthService\n ) {}\n\n /**\n * Resolve user from Bearer token (MCP OAuth).\n * When no token is provided, returns unauthenticated sentinel so\n * system profiles (organizationId=null) still work for unauthenticated MCP clients.\n */\n private async resolveUser(req: Request): Promise<{ id: string }> {\n const authHeader = req.headers.authorization;\n if (!authHeader?.startsWith('Bearer ')) {\n // No token — unauthenticated MCP client, can only access system profiles\n return { id: '__unauthenticated__' };\n }\n\n const token = authHeader.slice(7);\n const user = await this.authService.validateMcpToken(token);\n if (!user) {\n throw new UnauthorizedException('Invalid or expired MCP OAuth token');\n }\n\n return user;\n }\n\n // =========================================\n // Gateway Endpoints (must come BEFORE parameterized routes)\n // =========================================\n\n /**\n * SSE endpoint for gateway notifications (dedicated URL)\n */\n @Get('gateway/sse')\n streamGatewaySse(@Req() req: Request, @Res() res: Response) {\n return this.streamGatewayEvents(req, res);\n }\n\n /**\n * POST handler for gateway SSE\n */\n @Post('gateway/sse')\n @HttpCode(HttpStatus.OK)\n async handleGatewaySseRequest(\n @Req() req: Request,\n @Body() request: McpRequest\n ): Promise<McpResponse> {\n return this.handleGatewayRequest(req, request);\n }\n\n /**\n * Get gateway info\n */\n @Get('gateway/info')\n async getGatewayInfo(@Req() req: Request) {\n const user = await this.resolveUser(req);\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n\n try {\n const info = await this.proxyService.getProfileInfo(profileName, user.id);\n return {\n ...info,\n gateway: {\n activeProfile: profileName,\n },\n };\n } catch (error) {\n if (error instanceof NotFoundException) {\n throw new NotFoundException(\n `Default gateway profile \"${profileName}\" not found. Please select a valid profile in settings.`\n );\n }\n throw error;\n }\n }\n\n /**\n * GET handler for gateway endpoint\n */\n @Get('gateway')\n async getGatewayEndpoint(@Req() req: Request, @Res() res: Response) {\n const acceptHeader = req.headers.accept || '';\n if (acceptHeader.includes('text/event-stream')) {\n return this.streamGatewayEvents(req, res);\n }\n\n const user = await this.resolveUser(req);\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n\n try {\n const info = await this.proxyService.getProfileInfo(profileName, user.id);\n return res.json({\n message: 'This is the MCP Gateway endpoint. Use POST for JSON-RPC requests.',\n usage: {\n method: 'POST',\n contentType: 'application/json',\n body: { jsonrpc: '2.0', method: 'tools/list', id: 1 },\n },\n endpoints: {\n sse: '/api/mcp/gateway/sse',\n http: '/api/mcp/gateway',\n },\n gateway: {\n activeProfile: profileName,\n settingsEndpoint: '/api/settings/default-gateway-profile',\n },\n profile: {\n name: profileName,\n toolCount: info.tools.length,\n serverCount: info.serverStatus.total,\n connectedServers: info.serverStatus.connected,\n },\n infoEndpoint: '/api/mcp/gateway/info',\n });\n } catch (error) {\n if (error instanceof NotFoundException) {\n throw new NotFoundException(\n `Default gateway profile \"${profileName}\" not found. Please select a valid profile in settings.`\n );\n }\n throw error;\n }\n }\n\n /**\n * Stream SSE events for notifications.\n */\n private streamGatewayEvents(req: Request, res: Response) {\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n\n res.write(': connected\\n\\n');\n\n const subscription = fromEvent(this.eventEmitter, GATEWAY_PROFILE_CHANGED)\n .pipe(\n map(() => ({\n jsonrpc: '2.0',\n method: 'notifications/tools/list_changed',\n }))\n )\n .subscribe((notification) => {\n res.write(`data: ${JSON.stringify(notification)}\\n\\n`);\n });\n\n req.on('close', () => {\n subscription.unsubscribe();\n });\n }\n\n /**\n * MCP JSON-RPC endpoint for the gateway\n */\n @Post('gateway')\n @HttpCode(HttpStatus.OK)\n async handleGatewayRequest(\n @Req() req: Request,\n @Body() request: McpRequest\n ): Promise<McpResponse> {\n const user = await this.resolveUser(req);\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n\n try {\n return await this.proxyService.handleRequest(profileName, request, user.id);\n } catch (error) {\n if (error instanceof NotFoundException) {\n throw new NotFoundException(\n `Default gateway profile \"${profileName}\" not found. Please select a valid profile in settings.`\n );\n }\n throw error;\n }\n }\n\n // =========================================\n // Org-scoped Profile Endpoints: /api/mcp/:orgSlug/:profileName\n // =========================================\n\n /**\n * SSE endpoint for org-scoped profile\n */\n @Get(':orgSlug/:profileName/sse')\n async streamOrgProfileSse(\n @Param('orgSlug') orgSlug: string,\n @Param('profileName') profileName: string,\n @Req() req: Request,\n @Res() res: Response\n ) {\n await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);\n return this.streamGatewayEvents(req, res);\n }\n\n /**\n * POST handler for org-scoped profile SSE\n */\n @Post(':orgSlug/:profileName/sse')\n @HttpCode(HttpStatus.OK)\n async handleOrgProfileSseRequest(\n @Req() req: Request,\n @Param('orgSlug') orgSlug: string,\n @Param('profileName') profileName: string,\n @Body() request: McpRequest\n ): Promise<McpResponse> {\n return this.handleOrgMcpRequest(req, orgSlug, profileName, request);\n }\n\n /**\n * Get org-scoped profile info\n */\n @Get(':orgSlug/:profileName/info')\n async getOrgProfileInfo(\n @Param('orgSlug') orgSlug: string,\n @Param('profileName') profileName: string\n ) {\n return this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);\n }\n\n /**\n * GET handler for org-scoped MCP endpoint\n */\n @Get(':orgSlug/:profileName')\n async getOrgMcpEndpoint(\n @Param('orgSlug') orgSlug: string,\n @Param('profileName') profileName: string,\n @Req() req: Request,\n @Res() res: Response\n ) {\n const info = await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);\n\n const acceptHeader = req.headers.accept || '';\n if (acceptHeader.includes('text/event-stream')) {\n return this.streamGatewayEvents(req, res);\n }\n\n return res.json({\n message: 'This is an MCP (Model Context Protocol) endpoint. Use POST for JSON-RPC requests.',\n usage: {\n method: 'POST',\n contentType: 'application/json',\n body: { jsonrpc: '2.0', method: 'tools/list', id: 1 },\n },\n endpoints: {\n sse: `/api/mcp/${orgSlug}/${profileName}/sse`,\n http: `/api/mcp/${orgSlug}/${profileName}`,\n },\n profile: {\n name: profileName,\n toolCount: info.tools.length,\n serverCount: info.serverStatus.total,\n connectedServers: info.serverStatus.connected,\n },\n infoEndpoint: `/api/mcp/${orgSlug}/${profileName}/info`,\n });\n }\n\n /**\n * MCP JSON-RPC endpoint for an org-scoped profile\n */\n @Post(':orgSlug/:profileName')\n @HttpCode(HttpStatus.OK)\n async handleOrgMcpRequest(\n @Req() req: Request,\n @Param('orgSlug') orgSlug: string,\n @Param('profileName') profileName: string,\n @Body() request: McpRequest\n ): Promise<McpResponse> {\n const user = await this.resolveUser(req);\n return this.proxyService.handleRequestByOrgSlug(profileName, orgSlug, request, user.id);\n }\n}\n"],"names":["Body","Controller","Get","HttpCode","HttpStatus","NotFoundException","Param","Post","Req","Res","UnauthorizedException","EventEmitter2","fromEvent","map","AuthService","Public","GATEWAY_PROFILE_CHANGED","SettingsService","ProxyService","ProxyController","proxyService","settingsService","eventEmitter","authService","resolveUser","req","authHeader","headers","authorization","startsWith","id","token","slice","user","validateMcpToken","streamGatewaySse","res","streamGatewayEvents","handleGatewaySseRequest","request","handleGatewayRequest","getGatewayInfo","profileName","getDefaultGatewayProfile","info","getProfileInfo","gateway","activeProfile","error","getGatewayEndpoint","acceptHeader","accept","includes","json","message","usage","method","contentType","body","jsonrpc","endpoints","sse","http","settingsEndpoint","profile","name","toolCount","tools","length","serverCount","serverStatus","total","connectedServers","connected","infoEndpoint","setHeader","write","subscription","pipe","subscribe","notification","JSON","stringify","on","unsubscribe","handleRequest","streamOrgProfileSse","orgSlug","getProfileInfoByOrgSlug","handleOrgProfileSseRequest","handleOrgMcpRequest","getOrgProfileInfo","getOrgMcpEndpoint","handleRequestByOrgSlug","OK"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/proxy/proxy.controller.ts"],"sourcesContent":["/**\n * Proxy Controller\n *\n * MCP proxy endpoints for profiles and gateway.\n * Supports MCP Streamable HTTP transport (2025-11-25 spec) with SSE notifications.\n * Profile endpoints use org slug: /api/mcp/:orgSlug/:profileName\n *\n * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http\n */\n\nimport {\n Body,\n Controller,\n Get,\n HttpCode,\n HttpStatus,\n NotFoundException,\n Param,\n Post,\n Req,\n Res,\n UseGuards,\n} from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport type { Request, Response } from 'express';\nimport { fromEvent, map } from 'rxjs';\nimport { Public } from '../auth/decorators/public.decorator.js';\nimport { McpOAuthGuard } from '../auth/mcp-oauth.guard.js';\nimport { GATEWAY_PROFILE_CHANGED, SettingsService } from '../settings/settings.service.js';\nimport type { McpRequest, McpResponse } from './proxy.service.js';\nimport { ProxyService } from './proxy.service.js';\n\n@Public()\n@UseGuards(McpOAuthGuard)\n@Controller('mcp')\nexport class ProxyController {\n constructor(\n private readonly proxyService: ProxyService,\n private readonly settingsService: SettingsService,\n private readonly eventEmitter: EventEmitter2\n ) {}\n\n /**\n * Resolve user from request.\n * McpOAuthGuard always validates and attaches user before this is called.\n */\n private resolveUser(req: Request): { id: string } {\n return req.user!;\n }\n\n // =========================================\n // Gateway Endpoints (must come BEFORE parameterized routes)\n // =========================================\n\n /**\n * SSE endpoint for gateway notifications (dedicated URL)\n */\n @Get('gateway/sse')\n streamGatewaySse(@Req() req: Request, @Res() res: Response) {\n return this.streamGatewayEvents(req, res);\n }\n\n /**\n * POST handler for gateway SSE\n */\n @Post('gateway/sse')\n @HttpCode(HttpStatus.OK)\n async handleGatewaySseRequest(\n @Req() req: Request,\n @Body() request: McpRequest\n ): Promise<McpResponse> {\n return this.handleGatewayRequest(req, request);\n }\n\n /**\n * Get gateway info\n */\n @Get('gateway/info')\n async getGatewayInfo(@Req() req: Request) {\n const user = this.resolveUser(req);\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n\n try {\n const info = await this.proxyService.getProfileInfo(profileName, user.id);\n return {\n ...info,\n gateway: {\n activeProfile: profileName,\n },\n };\n } catch (error) {\n if (error instanceof NotFoundException) {\n throw new NotFoundException(\n `Default gateway profile \"${profileName}\" not found. Please select a valid profile in settings.`\n );\n }\n throw error;\n }\n }\n\n /**\n * GET handler for gateway endpoint\n */\n @Get('gateway')\n async getGatewayEndpoint(@Req() req: Request, @Res() res: Response) {\n const acceptHeader = req.headers.accept || '';\n if (acceptHeader.includes('text/event-stream')) {\n return this.streamGatewayEvents(req, res);\n }\n\n const user = this.resolveUser(req);\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n\n try {\n const info = await this.proxyService.getProfileInfo(profileName, user.id);\n return res.json({\n message: 'This is the MCP Gateway endpoint. Use POST for JSON-RPC requests.',\n usage: {\n method: 'POST',\n contentType: 'application/json',\n body: { jsonrpc: '2.0', method: 'tools/list', id: 1 },\n },\n endpoints: {\n sse: '/api/mcp/gateway/sse',\n http: '/api/mcp/gateway',\n },\n gateway: {\n activeProfile: profileName,\n settingsEndpoint: '/api/settings/default-gateway-profile',\n },\n profile: {\n name: profileName,\n toolCount: info.tools.length,\n serverCount: info.serverStatus.total,\n connectedServers: info.serverStatus.connected,\n },\n infoEndpoint: '/api/mcp/gateway/info',\n });\n } catch (error) {\n if (error instanceof NotFoundException) {\n throw new NotFoundException(\n `Default gateway profile \"${profileName}\" not found. Please select a valid profile in settings.`\n );\n }\n throw error;\n }\n }\n\n /**\n * Stream SSE events for notifications.\n */\n private streamGatewayEvents(req: Request, res: Response) {\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n\n res.write(': connected\\n\\n');\n\n const subscription = fromEvent(this.eventEmitter, GATEWAY_PROFILE_CHANGED)\n .pipe(\n map(() => ({\n jsonrpc: '2.0',\n method: 'notifications/tools/list_changed',\n }))\n )\n .subscribe((notification) => {\n res.write(`data: ${JSON.stringify(notification)}\\n\\n`);\n });\n\n req.on('close', () => {\n subscription.unsubscribe();\n });\n }\n\n /**\n * MCP JSON-RPC endpoint for the gateway\n */\n @Post('gateway')\n @HttpCode(HttpStatus.OK)\n async handleGatewayRequest(\n @Req() req: Request,\n @Body() request: McpRequest\n ): Promise<McpResponse> {\n const user = this.resolveUser(req);\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n\n try {\n return await this.proxyService.handleRequest(profileName, request, user.id);\n } catch (error) {\n if (error instanceof NotFoundException) {\n throw new NotFoundException(\n `Default gateway profile \"${profileName}\" not found. Please select a valid profile in settings.`\n );\n }\n throw error;\n }\n }\n\n // =========================================\n // Org-scoped Gateway Endpoints: /api/mcp/:orgSlug/gateway\n // (must come BEFORE :orgSlug/:profileName to avoid \"gateway\" matching :profileName)\n // =========================================\n\n /**\n * SSE endpoint for org-scoped gateway\n */\n @Get(':orgSlug/gateway/sse')\n async streamOrgGatewaySse(\n @Param('orgSlug') orgSlug: string,\n @Req() req: Request,\n @Res() res: Response\n ) {\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);\n return this.streamGatewayEvents(req, res);\n }\n\n /**\n * POST handler for org-scoped gateway SSE\n */\n @Post(':orgSlug/gateway/sse')\n @HttpCode(HttpStatus.OK)\n async handleOrgGatewaySseRequest(\n @Req() req: Request,\n @Param('orgSlug') orgSlug: string,\n @Body() request: McpRequest\n ): Promise<McpResponse> {\n const user = this.resolveUser(req);\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n return this.proxyService.handleRequestByOrgSlug(profileName, orgSlug, request, user.id);\n }\n\n /**\n * Get org-scoped gateway info\n */\n @Get(':orgSlug/gateway/info')\n async getOrgGatewayInfo(@Param('orgSlug') orgSlug: string) {\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n const info = await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);\n return {\n ...info,\n gateway: {\n activeProfile: profileName,\n },\n };\n }\n\n /**\n * GET handler for org-scoped gateway endpoint\n */\n @Get(':orgSlug/gateway')\n async getOrgGatewayEndpoint(\n @Param('orgSlug') orgSlug: string,\n @Req() req: Request,\n @Res() res: Response\n ) {\n const acceptHeader = req.headers.accept || '';\n if (acceptHeader.includes('text/event-stream')) {\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);\n return this.streamGatewayEvents(req, res);\n }\n\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n const info = await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);\n\n return res.json({\n message: 'This is the MCP Gateway endpoint. Use POST for JSON-RPC requests.',\n usage: {\n method: 'POST',\n contentType: 'application/json',\n body: { jsonrpc: '2.0', method: 'tools/list', id: 1 },\n },\n endpoints: {\n sse: `/api/mcp/${orgSlug}/gateway/sse`,\n http: `/api/mcp/${orgSlug}/gateway`,\n },\n gateway: {\n activeProfile: profileName,\n settingsEndpoint: '/api/settings/default-gateway-profile',\n },\n profile: {\n name: profileName,\n toolCount: info.tools.length,\n serverCount: info.serverStatus.total,\n connectedServers: info.serverStatus.connected,\n },\n infoEndpoint: `/api/mcp/${orgSlug}/gateway/info`,\n });\n }\n\n /**\n * MCP JSON-RPC endpoint for org-scoped gateway\n */\n @Post(':orgSlug/gateway')\n @HttpCode(HttpStatus.OK)\n async handleOrgGatewayRequest(\n @Req() req: Request,\n @Param('orgSlug') orgSlug: string,\n @Body() request: McpRequest\n ): Promise<McpResponse> {\n const user = this.resolveUser(req);\n const profileName = await this.settingsService.getDefaultGatewayProfile();\n return this.proxyService.handleRequestByOrgSlug(profileName, orgSlug, request, user.id);\n }\n\n // =========================================\n // Org-scoped Profile Endpoints: /api/mcp/:orgSlug/:profileName\n // =========================================\n\n /**\n * SSE endpoint for org-scoped profile\n */\n @Get(':orgSlug/:profileName/sse')\n async streamOrgProfileSse(\n @Param('orgSlug') orgSlug: string,\n @Param('profileName') profileName: string,\n @Req() req: Request,\n @Res() res: Response\n ) {\n await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);\n return this.streamGatewayEvents(req, res);\n }\n\n /**\n * POST handler for org-scoped profile SSE\n */\n @Post(':orgSlug/:profileName/sse')\n @HttpCode(HttpStatus.OK)\n async handleOrgProfileSseRequest(\n @Req() req: Request,\n @Param('orgSlug') orgSlug: string,\n @Param('profileName') profileName: string,\n @Body() request: McpRequest\n ): Promise<McpResponse> {\n return this.handleOrgMcpRequest(req, orgSlug, profileName, request);\n }\n\n /**\n * Get org-scoped profile info\n */\n @Get(':orgSlug/:profileName/info')\n async getOrgProfileInfo(\n @Param('orgSlug') orgSlug: string,\n @Param('profileName') profileName: string\n ) {\n return this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);\n }\n\n /**\n * GET handler for org-scoped MCP endpoint\n */\n @Get(':orgSlug/:profileName')\n async getOrgMcpEndpoint(\n @Param('orgSlug') orgSlug: string,\n @Param('profileName') profileName: string,\n @Req() req: Request,\n @Res() res: Response\n ) {\n const info = await this.proxyService.getProfileInfoByOrgSlug(profileName, orgSlug);\n\n const acceptHeader = req.headers.accept || '';\n if (acceptHeader.includes('text/event-stream')) {\n return this.streamGatewayEvents(req, res);\n }\n\n return res.json({\n message: 'This is an MCP (Model Context Protocol) endpoint. Use POST for JSON-RPC requests.',\n usage: {\n method: 'POST',\n contentType: 'application/json',\n body: { jsonrpc: '2.0', method: 'tools/list', id: 1 },\n },\n endpoints: {\n sse: `/api/mcp/${orgSlug}/${profileName}/sse`,\n http: `/api/mcp/${orgSlug}/${profileName}`,\n },\n profile: {\n name: profileName,\n toolCount: info.tools.length,\n serverCount: info.serverStatus.total,\n connectedServers: info.serverStatus.connected,\n },\n infoEndpoint: `/api/mcp/${orgSlug}/${profileName}/info`,\n });\n }\n\n /**\n * MCP JSON-RPC endpoint for an org-scoped profile\n */\n @Post(':orgSlug/:profileName')\n @HttpCode(HttpStatus.OK)\n async handleOrgMcpRequest(\n @Req() req: Request,\n @Param('orgSlug') orgSlug: string,\n @Param('profileName') profileName: string,\n @Body() request: McpRequest\n ): Promise<McpResponse> {\n const user = this.resolveUser(req);\n return this.proxyService.handleRequestByOrgSlug(profileName, orgSlug, request, user.id);\n }\n}\n"],"names":["Body","Controller","Get","HttpCode","HttpStatus","NotFoundException","Param","Post","Req","Res","UseGuards","EventEmitter2","fromEvent","map","Public","McpOAuthGuard","GATEWAY_PROFILE_CHANGED","SettingsService","ProxyService","ProxyController","proxyService","settingsService","eventEmitter","resolveUser","req","user","streamGatewaySse","res","streamGatewayEvents","handleGatewaySseRequest","request","handleGatewayRequest","getGatewayInfo","profileName","getDefaultGatewayProfile","info","getProfileInfo","id","gateway","activeProfile","error","getGatewayEndpoint","acceptHeader","headers","accept","includes","json","message","usage","method","contentType","body","jsonrpc","endpoints","sse","http","settingsEndpoint","profile","name","toolCount","tools","length","serverCount","serverStatus","total","connectedServers","connected","infoEndpoint","setHeader","write","subscription","pipe","subscribe","notification","JSON","stringify","on","unsubscribe","handleRequest","streamOrgGatewaySse","orgSlug","getProfileInfoByOrgSlug","handleOrgGatewaySseRequest","handleRequestByOrgSlug","getOrgGatewayInfo","getOrgGatewayEndpoint","handleOrgGatewayRequest","streamOrgProfileSse","handleOrgProfileSseRequest","handleOrgMcpRequest","getOrgProfileInfo","getOrgMcpEndpoint","OK"],"mappings":"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"}
|
|
@@ -296,12 +296,35 @@ export class ProxyService {
|
|
|
296
296
|
return instance;
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
|
+
// For remote_http and remote_sse servers, look up OAuth token
|
|
300
|
+
let oauthToken = null;
|
|
301
|
+
if (server.type === 'remote_http' || server.type === 'remote_sse') {
|
|
302
|
+
const tokenRecord = await this.prisma.oAuthToken.findUnique({
|
|
303
|
+
where: {
|
|
304
|
+
mcpServerId: server.id
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
if (tokenRecord) {
|
|
308
|
+
oauthToken = {
|
|
309
|
+
id: tokenRecord.id,
|
|
310
|
+
mcpServerId: tokenRecord.mcpServerId,
|
|
311
|
+
accessToken: tokenRecord.accessToken,
|
|
312
|
+
tokenType: tokenRecord.tokenType,
|
|
313
|
+
refreshToken: tokenRecord.refreshToken ?? undefined,
|
|
314
|
+
scope: tokenRecord.scope ?? undefined,
|
|
315
|
+
expiresAt: tokenRecord.expiresAt?.getTime(),
|
|
316
|
+
createdAt: tokenRecord.createdAt.getTime(),
|
|
317
|
+
updatedAt: tokenRecord.updatedAt.getTime()
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
299
321
|
// For remote_http servers, create RemoteHttpMcpServer
|
|
300
322
|
if (server.type === 'remote_http' && config?.url) {
|
|
301
323
|
const remoteServer = new RemoteHttpMcpServer({
|
|
302
324
|
url: config.url,
|
|
303
|
-
transport: 'http'
|
|
304
|
-
|
|
325
|
+
transport: 'http',
|
|
326
|
+
headers: config.headers
|
|
327
|
+
}, oauthToken, apiKeyConfig);
|
|
305
328
|
await remoteServer.initialize();
|
|
306
329
|
this.serverInstances.set(server.id, remoteServer);
|
|
307
330
|
return remoteServer;
|
|
@@ -310,8 +333,9 @@ export class ProxyService {
|
|
|
310
333
|
if (server.type === 'remote_sse' && config?.url) {
|
|
311
334
|
const remoteServer = new RemoteSseMcpServer({
|
|
312
335
|
url: config.url,
|
|
313
|
-
transport: 'sse'
|
|
314
|
-
|
|
336
|
+
transport: 'sse',
|
|
337
|
+
headers: config.headers
|
|
338
|
+
}, oauthToken, apiKeyConfig);
|
|
315
339
|
await remoteServer.initialize();
|
|
316
340
|
this.serverInstances.set(server.id, remoteServer);
|
|
317
341
|
return remoteServer;
|