@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
|
@@ -253,7 +253,8 @@ export class McpService {
|
|
|
253
253
|
const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig) : null;
|
|
254
254
|
const remoteServer = new RemoteHttpMcpServer({
|
|
255
255
|
url: config.url,
|
|
256
|
-
transport: 'http'
|
|
256
|
+
transport: 'http',
|
|
257
|
+
headers: config.headers
|
|
257
258
|
}, null, apiKeyConfig);
|
|
258
259
|
await remoteServer.initialize();
|
|
259
260
|
const tools = await remoteServer.listTools();
|
|
@@ -267,7 +268,8 @@ export class McpService {
|
|
|
267
268
|
const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig) : null;
|
|
268
269
|
const remoteServer = new RemoteSseMcpServer({
|
|
269
270
|
url: config.url,
|
|
270
|
-
transport: 'sse'
|
|
271
|
+
transport: 'sse',
|
|
272
|
+
headers: config.headers
|
|
271
273
|
}, null, apiKeyConfig);
|
|
272
274
|
await remoteServer.initialize();
|
|
273
275
|
const tools = await remoteServer.listTools();
|
|
@@ -382,14 +384,28 @@ export class McpService {
|
|
|
382
384
|
validationDetails = 'Server ready (no API key required)';
|
|
383
385
|
}
|
|
384
386
|
// For remote_http servers, validate by connecting
|
|
387
|
+
let oauthRequired = false;
|
|
385
388
|
if (server.type === 'remote_http' && status === 'unknown') {
|
|
386
389
|
const config = this.parseConfig(server.config);
|
|
387
390
|
const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig) : null;
|
|
391
|
+
// Get OAuth token if available, mapping Prisma types to core OAuthToken
|
|
392
|
+
const oauthToken = server.oauthToken ? {
|
|
393
|
+
id: server.oauthToken.id,
|
|
394
|
+
mcpServerId: server.oauthToken.mcpServerId,
|
|
395
|
+
accessToken: server.oauthToken.accessToken,
|
|
396
|
+
tokenType: server.oauthToken.tokenType,
|
|
397
|
+
refreshToken: server.oauthToken.refreshToken ?? undefined,
|
|
398
|
+
scope: server.oauthToken.scope ?? undefined,
|
|
399
|
+
expiresAt: server.oauthToken.expiresAt?.getTime(),
|
|
400
|
+
createdAt: server.oauthToken.createdAt.getTime(),
|
|
401
|
+
updatedAt: server.oauthToken.updatedAt.getTime()
|
|
402
|
+
} : null;
|
|
388
403
|
try {
|
|
389
404
|
const remoteServer = new RemoteHttpMcpServer({
|
|
390
405
|
url: config.url,
|
|
391
|
-
transport: 'http'
|
|
392
|
-
|
|
406
|
+
transport: 'http',
|
|
407
|
+
headers: config.headers
|
|
408
|
+
}, oauthToken, apiKeyConfig);
|
|
393
409
|
await remoteServer.initialize();
|
|
394
410
|
const tools = await remoteServer.listTools();
|
|
395
411
|
status = 'connected';
|
|
@@ -397,18 +413,36 @@ export class McpService {
|
|
|
397
413
|
} catch (error) {
|
|
398
414
|
status = 'error';
|
|
399
415
|
validationError = error instanceof Error ? error.message : 'Unknown error';
|
|
400
|
-
|
|
416
|
+
if (validationError.includes('OAUTH_REQUIRED')) {
|
|
417
|
+
oauthRequired = true;
|
|
418
|
+
validationDetails = 'OAuth authentication required. Click "Login with OAuth" to authorize.';
|
|
419
|
+
} else {
|
|
420
|
+
validationDetails = `Connection failed: ${validationError}`;
|
|
421
|
+
}
|
|
401
422
|
}
|
|
402
423
|
}
|
|
403
424
|
// For remote_sse servers, validate by connecting
|
|
404
425
|
if (server.type === 'remote_sse' && status === 'unknown') {
|
|
405
426
|
const config = this.parseConfig(server.config);
|
|
406
427
|
const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig) : null;
|
|
428
|
+
// Get OAuth token if available, mapping Prisma types to core OAuthToken
|
|
429
|
+
const oauthToken = server.oauthToken ? {
|
|
430
|
+
id: server.oauthToken.id,
|
|
431
|
+
mcpServerId: server.oauthToken.mcpServerId,
|
|
432
|
+
accessToken: server.oauthToken.accessToken,
|
|
433
|
+
tokenType: server.oauthToken.tokenType,
|
|
434
|
+
refreshToken: server.oauthToken.refreshToken ?? undefined,
|
|
435
|
+
scope: server.oauthToken.scope ?? undefined,
|
|
436
|
+
expiresAt: server.oauthToken.expiresAt?.getTime(),
|
|
437
|
+
createdAt: server.oauthToken.createdAt.getTime(),
|
|
438
|
+
updatedAt: server.oauthToken.updatedAt.getTime()
|
|
439
|
+
} : null;
|
|
407
440
|
try {
|
|
408
441
|
const remoteServer = new RemoteSseMcpServer({
|
|
409
442
|
url: config.url,
|
|
410
|
-
transport: 'sse'
|
|
411
|
-
|
|
443
|
+
transport: 'sse',
|
|
444
|
+
headers: config.headers
|
|
445
|
+
}, oauthToken, apiKeyConfig);
|
|
412
446
|
await remoteServer.initialize();
|
|
413
447
|
const tools = await remoteServer.listTools();
|
|
414
448
|
status = 'connected';
|
|
@@ -416,7 +450,12 @@ export class McpService {
|
|
|
416
450
|
} catch (error) {
|
|
417
451
|
status = 'error';
|
|
418
452
|
validationError = error instanceof Error ? error.message : 'Unknown error';
|
|
419
|
-
|
|
453
|
+
if (validationError.includes('OAUTH_REQUIRED')) {
|
|
454
|
+
oauthRequired = true;
|
|
455
|
+
validationDetails = 'OAuth authentication required. Click "Login with OAuth" to authorize.';
|
|
456
|
+
} else {
|
|
457
|
+
validationDetails = `Connection failed: ${validationError}`;
|
|
458
|
+
}
|
|
420
459
|
}
|
|
421
460
|
}
|
|
422
461
|
// For external (NPX/stdio) servers, validate by spawning and checking
|
|
@@ -451,6 +490,7 @@ export class McpService {
|
|
|
451
490
|
hasOAuth,
|
|
452
491
|
requiresApiKey,
|
|
453
492
|
requiresOAuth,
|
|
493
|
+
oauthRequired,
|
|
454
494
|
isReady,
|
|
455
495
|
status,
|
|
456
496
|
error: validationError,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/mcp/mcp.service.ts"],"sourcesContent":["/**\n * MCP Service\n *\n * Business logic for MCP server management.\n * All queries are scoped to the user's active organization.\n * System records (organizationId=null, including builtin) are visible to all.\n */\n\nimport {\n ExternalMcpServer,\n RemoteHttpMcpServer,\n RemoteSseMcpServer,\n} from '@dxheroes/local-mcp-core';\nimport { ConflictException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';\n\n/** Sentinel value for unauthenticated MCP access — can only see system servers */\nconst UNAUTHENTICATED_ID = '__unauthenticated__';\n\nimport { PrismaService } from '../database/prisma.service.js';\nimport { DebugService } from '../debug/debug.service.js';\nimport type { CreateMcpServerDto } from './dto/create-mcp-server.dto.js';\nimport type { UpdateMcpServerDto } from './dto/update-mcp-server.dto.js';\nimport { MCP_PRESETS } from './mcp-presets.js';\nimport { McpRegistry } from './mcp-registry.js';\n\n@Injectable()\nexport class McpService {\n constructor(\n private readonly prisma: PrismaService,\n private readonly registry: McpRegistry,\n private readonly debugService: DebugService\n ) {}\n\n private isAnonymous(userId: string): boolean {\n return userId === UNAUTHENTICATED_ID;\n }\n\n private async assertAccess(serverId: string, userId: string, orgId?: string): Promise<void> {\n if (this.isAnonymous(userId)) return;\n\n const server = await this.prisma.mcpServer.findUnique({\n where: { id: serverId },\n select: { userId: true, organizationId: true },\n });\n if (!server) throw new NotFoundException(`MCP server ${serverId} not found`);\n\n // System record (no org) — accessible to all\n if (!server.organizationId) return;\n\n // Must belong to the active org\n if (orgId && server.organizationId === orgId) return;\n\n throw new ForbiddenException('You do not have access to this MCP server');\n }\n\n private async assertOwnership(serverId: string, userId: string, orgId?: string): Promise<void> {\n if (this.isAnonymous(userId)) return;\n\n const server = await this.prisma.mcpServer.findUnique({\n where: { id: serverId },\n select: { userId: true, organizationId: true },\n });\n if (!server) throw new NotFoundException(`MCP server ${serverId} not found`);\n if (!server.organizationId) return; // system record\n\n if (orgId && server.organizationId === orgId) return;\n\n throw new ForbiddenException('You do not own this MCP server');\n }\n\n /**\n * Get all MCP servers visible in the active org (org servers + system servers)\n */\n async findAll(userId: string, orgId?: string) {\n const include = { profiles: { include: { profile: true as const } } };\n const orderBy = { name: 'asc' as const };\n\n const servers = this.isAnonymous(userId)\n ? await this.prisma.mcpServer.findMany({ include, orderBy })\n : await this.prisma.mcpServer.findMany({\n where: {\n OR: [\n { organizationId: orgId }, // org servers\n { organizationId: null }, // system servers\n ],\n },\n include,\n orderBy,\n });\n\n // Enrich with metadata from registry for builtin servers\n return servers.map((server) => {\n const builtinId = this.getBuiltinId(server.config);\n return builtinId && this.registry.has(builtinId)\n ? { ...server, metadata: this.registry.get(builtinId)?.metadata }\n : server;\n });\n }\n\n /**\n * Get a specific MCP server\n */\n async findById(id: string, userId?: string, orgId?: string) {\n if (userId) {\n await this.assertAccess(id, userId, orgId);\n }\n\n const server = await this.prisma.mcpServer.findUnique({\n where: { id },\n include: {\n profiles: {\n include: {\n profile: true,\n tools: true,\n },\n },\n oauthToken: true,\n toolsCache: true,\n },\n });\n\n if (!server) {\n throw new NotFoundException(`MCP server ${id} not found`);\n }\n\n // Enrich with metadata from registry for builtin servers\n const builtinId = this.getBuiltinId(server.config);\n\n return builtinId && this.registry.has(builtinId)\n ? { ...server, metadata: this.registry.get(builtinId)?.metadata }\n : server;\n }\n\n /**\n * Create a new MCP server in the active org\n */\n async create(dto: CreateMcpServerDto, userId: string, orgId?: string) {\n return this.prisma.mcpServer.create({\n data: {\n name: dto.name,\n type: dto.type,\n config: JSON.stringify(dto.config || {}),\n apiKeyConfig: dto.apiKeyConfig ? JSON.stringify(dto.apiKeyConfig) : null,\n oauthConfig: dto.oauthConfig ? JSON.stringify(dto.oauthConfig) : null,\n userId: this.isAnonymous(userId) ? null : userId,\n organizationId: orgId ?? null,\n },\n });\n }\n\n /**\n * Update an MCP server\n */\n async update(id: string, dto: UpdateMcpServerDto, userId: string, orgId?: string) {\n await this.assertOwnership(id, userId, orgId);\n\n const server = await this.prisma.mcpServer.findUnique({ where: { id } });\n if (!server) {\n throw new NotFoundException(`MCP server ${id} not found`);\n }\n\n return this.prisma.mcpServer.update({\n where: { id },\n data: {\n name: dto.name,\n type: dto.type,\n config:\n dto.config !== undefined\n ? typeof dto.config === 'string'\n ? dto.config\n : JSON.stringify(dto.config)\n : undefined,\n apiKeyConfig: dto.apiKeyConfig !== undefined ? JSON.stringify(dto.apiKeyConfig) : undefined,\n oauthConfig: dto.oauthConfig !== undefined ? JSON.stringify(dto.oauthConfig) : undefined,\n },\n });\n }\n\n /**\n * Delete an MCP server\n */\n async delete(id: string, userId: string, orgId?: string) {\n await this.assertOwnership(id, userId, orgId);\n\n const server = await this.prisma.mcpServer.findUnique({ where: { id } });\n if (!server) {\n throw new NotFoundException(`MCP server ${id} not found`);\n }\n\n await this.prisma.mcpServer.delete({ where: { id } });\n }\n\n /**\n * Get tools from an MCP server\n */\n async getTools(id: string, userId?: string, orgId?: string) {\n if (userId) {\n await this.assertAccess(id, userId, orgId);\n }\n\n const startTime = Date.now();\n\n // Create debug log entry\n const log = await this.debugService.createLog({\n mcpServerId: id,\n requestType: 'tools/list',\n requestPayload: JSON.stringify({ method: 'tools/list', serverId: id }),\n status: 'pending',\n });\n\n try {\n const result = await this.getToolsInternal(id);\n\n // Update debug log with success\n await this.debugService.updateLog(log.id, {\n responsePayload: JSON.stringify(result),\n status: 'success',\n durationMs: Date.now() - startTime,\n });\n\n return result;\n } catch (error) {\n // Update debug log with error\n await this.debugService.updateLog(log.id, {\n status: 'error',\n errorMessage: error instanceof Error ? error.message : 'Unknown error',\n durationMs: Date.now() - startTime,\n });\n throw error;\n }\n }\n\n /**\n * Internal method to get tools from an MCP server\n */\n private async getToolsInternal(id: string) {\n const server = await this.findById(id);\n const builtinId = this.getBuiltinId(server.config);\n\n // For builtin servers, get tools from the registry\n if (builtinId && this.registry.has(builtinId)) {\n const pkg = this.registry.get(builtinId);\n if (pkg) {\n // Get API key config if set\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n // Create server instance and list tools\n const instance = pkg.createServer(apiKeyConfig);\n await instance.initialize();\n const tools = await instance.listTools();\n return { tools };\n }\n }\n\n // For remote_http servers, connect and fetch tools\n if (server.type === 'remote_http') {\n const config = this.parseConfig(server.config) as { url: string };\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n const remoteServer = new RemoteHttpMcpServer(\n { url: config.url, transport: 'http' },\n null,\n apiKeyConfig\n );\n await remoteServer.initialize();\n const tools = await remoteServer.listTools();\n return { tools };\n }\n\n // For remote_sse servers, connect and fetch tools\n if (server.type === 'remote_sse') {\n const config = this.parseConfig(server.config) as { url: string };\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n const remoteServer = new RemoteSseMcpServer(\n { url: config.url, transport: 'sse' },\n null,\n apiKeyConfig\n );\n await remoteServer.initialize();\n const tools = await remoteServer.listTools();\n return { tools };\n }\n\n // For external (NPX/stdio) servers, spawn and fetch tools\n if (server.type === 'external') {\n const config = this.parseConfig(server.config) as {\n command: string;\n args?: string[];\n env?: Record<string, string>;\n workingDirectory?: string;\n autoRestart?: boolean;\n maxRestartAttempts?: number;\n startupTimeout?: number;\n shutdownTimeout?: number;\n };\n\n const externalServer = new ExternalMcpServer(config);\n try {\n await externalServer.initialize();\n const tools = await externalServer.listTools();\n return { tools };\n } finally {\n // Shutdown after fetching tools\n await externalServer.shutdown();\n }\n }\n\n // For cached tools from external servers (fallback)\n const tools = await this.prisma.mcpServerToolsCache.findMany({\n where: { mcpServerId: id },\n });\n return { tools };\n }\n\n /**\n * Get MCP server status with real validation\n */\n async getStatus(id: string, userId?: string, orgId?: string) {\n if (userId) {\n await this.assertAccess(id, userId, orgId);\n }\n\n const startTime = Date.now();\n\n // Create debug log entry\n const log = await this.debugService.createLog({\n mcpServerId: id,\n requestType: 'status/check',\n requestPayload: JSON.stringify({ method: 'status/check', serverId: id }),\n status: 'pending',\n });\n\n try {\n const result = await this.getStatusInternal(id);\n\n // Update debug log with success or error based on status\n await this.debugService.updateLog(log.id, {\n responsePayload: JSON.stringify(result),\n status: result.status === 'error' ? 'error' : 'success',\n errorMessage: result.error,\n durationMs: Date.now() - startTime,\n });\n\n return result;\n } catch (error) {\n // Update debug log with error\n await this.debugService.updateLog(log.id, {\n status: 'error',\n errorMessage: error instanceof Error ? error.message : 'Unknown error',\n durationMs: Date.now() - startTime,\n });\n throw error;\n }\n }\n\n /**\n * Internal method to get MCP server status with real validation\n */\n private async getStatusInternal(id: string) {\n const server = await this.findById(id);\n const builtinId = this.getBuiltinId(server.config);\n\n // Check if it's a builtin server\n const isBuiltin = builtinId && this.registry.has(builtinId);\n\n // Check API key config\n const hasApiKey = !!server.apiKeyConfig;\n\n // Check OAuth token\n const hasOAuth = !!server.oauthToken;\n\n // Get metadata\n const metadata = isBuiltin ? this.registry.get(builtinId)?.metadata : null;\n const requiresApiKey = metadata?.requiresApiKey ?? false;\n const requiresOAuth = metadata?.requiresOAuth ?? false;\n\n // Default status\n let status: 'connected' | 'error' | 'unknown' = 'unknown';\n let validationError: string | undefined;\n let validationDetails: string | undefined;\n\n // For builtin servers with API key, actually validate\n if (isBuiltin && hasApiKey) {\n const pkg = this.registry.get(builtinId);\n if (pkg) {\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n try {\n const instance = pkg.createServer(apiKeyConfig);\n const validation = await instance.validate();\n status = validation.valid ? 'connected' : 'error';\n validationError = validation.error;\n validationDetails = validation.valid\n ? 'API key validated successfully'\n : `Validation failed: ${validation.error}`;\n } catch (error) {\n status = 'error';\n validationError = error instanceof Error ? error.message : 'Unknown error';\n validationDetails = `Connection test failed: ${validationError}`;\n }\n }\n } else if (!hasApiKey && requiresApiKey) {\n status = 'error';\n validationError = 'API key required';\n validationDetails = 'This server requires an API key to function';\n } else if (isBuiltin && !requiresApiKey) {\n status = 'connected';\n validationDetails = 'Server ready (no API key required)';\n }\n\n // For remote_http servers, validate by connecting\n if (server.type === 'remote_http' && status === 'unknown') {\n const config = this.parseConfig(server.config) as { url: string };\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n try {\n const remoteServer = new RemoteHttpMcpServer(\n { url: config.url, transport: 'http' },\n null,\n apiKeyConfig\n );\n await remoteServer.initialize();\n const tools = await remoteServer.listTools();\n status = 'connected';\n validationDetails = `Connected successfully. ${tools.length} tools available.`;\n } catch (error) {\n status = 'error';\n validationError = error instanceof Error ? error.message : 'Unknown error';\n validationDetails = `Connection failed: ${validationError}`;\n }\n }\n\n // For remote_sse servers, validate by connecting\n if (server.type === 'remote_sse' && status === 'unknown') {\n const config = this.parseConfig(server.config) as { url: string };\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n try {\n const remoteServer = new RemoteSseMcpServer(\n { url: config.url, transport: 'sse' },\n null,\n apiKeyConfig\n );\n await remoteServer.initialize();\n const tools = await remoteServer.listTools();\n status = 'connected';\n validationDetails = `Connected successfully (SSE). ${tools.length} tools available.`;\n } catch (error) {\n status = 'error';\n validationError = error instanceof Error ? error.message : 'Unknown error';\n validationDetails = `Connection failed: ${validationError}`;\n }\n }\n\n // For external (NPX/stdio) servers, validate by spawning and checking\n if (server.type === 'external' && status === 'unknown') {\n const config = this.parseConfig(server.config) as {\n command: string;\n args?: string[];\n env?: Record<string, string>;\n workingDirectory?: string;\n };\n\n if (!config.command) {\n status = 'error';\n validationError = 'Command is required for external MCP servers';\n validationDetails = 'Missing command configuration';\n } else {\n try {\n const externalServer = new ExternalMcpServer(config);\n await externalServer.initialize();\n const tools = await externalServer.listTools();\n status = 'connected';\n validationDetails = `Connected successfully (stdio). ${tools.length} tools available.`;\n await externalServer.shutdown();\n } catch (error) {\n status = 'error';\n validationError = error instanceof Error ? error.message : 'Unknown error';\n validationDetails = `Connection failed: ${validationError}`;\n }\n }\n }\n\n const isReady = status === 'connected';\n\n return {\n id: server.id,\n name: server.name,\n type: server.type,\n isBuiltin,\n hasApiKey,\n hasOAuth,\n requiresApiKey,\n requiresOAuth,\n isReady,\n status,\n error: validationError,\n details: validationDetails,\n validatedAt: new Date().toISOString(),\n };\n }\n\n /**\n * Add a preset MCP server to the user's organization\n */\n async addPreset(presetId: string, userId: string, orgId: string) {\n const preset = MCP_PRESETS.find((p) => p.id === presetId);\n if (!preset) {\n throw new NotFoundException(`Preset \"${presetId}\" not found`);\n }\n\n // Check if a server with this name already exists in the org\n const existing = await this.prisma.mcpServer.findFirst({\n where: {\n name: preset.name,\n organizationId: orgId,\n },\n });\n\n if (existing) {\n throw new ConflictException(\n `MCP server \"${preset.name}\" already exists in your organization`\n );\n }\n\n return this.prisma.mcpServer.create({\n data: {\n name: preset.name,\n type: preset.type,\n config: JSON.stringify(preset.config),\n userId,\n organizationId: orgId,\n },\n });\n }\n\n /**\n * Extract builtinId from config (handles JSON string parsing)\n */\n private getBuiltinId(config: unknown): string | null {\n const parsed = this.parseConfig(config);\n const builtinId = parsed?.builtinId;\n\n if (typeof builtinId === 'string') {\n return builtinId;\n }\n return null;\n }\n\n private parseConfig(config: unknown): Record<string, unknown> | null {\n if (typeof config === 'string') {\n try {\n return JSON.parse(config);\n } catch {\n return null;\n }\n }\n return config as Record<string, unknown> | null;\n }\n}\n"],"names":["ExternalMcpServer","RemoteHttpMcpServer","RemoteSseMcpServer","ConflictException","ForbiddenException","Injectable","NotFoundException","UNAUTHENTICATED_ID","PrismaService","DebugService","MCP_PRESETS","McpRegistry","McpService","prisma","registry","debugService","isAnonymous","userId","assertAccess","serverId","orgId","server","mcpServer","findUnique","where","id","select","organizationId","assertOwnership","findAll","include","profiles","profile","orderBy","name","servers","findMany","OR","map","builtinId","getBuiltinId","config","has","metadata","get","findById","tools","oauthToken","toolsCache","create","dto","data","type","JSON","stringify","apiKeyConfig","oauthConfig","update","undefined","delete","getTools","startTime","Date","now","log","createLog","mcpServerId","requestType","requestPayload","method","status","result","getToolsInternal","updateLog","responsePayload","durationMs","error","errorMessage","Error","message","pkg","parse","instance","createServer","initialize","listTools","parseConfig","remoteServer","url","transport","externalServer","shutdown","mcpServerToolsCache","getStatus","getStatusInternal","isBuiltin","hasApiKey","hasOAuth","requiresApiKey","requiresOAuth","validationError","validationDetails","validation","validate","valid","length","command","isReady","details","validatedAt","toISOString","addPreset","presetId","preset","find","p","existing","findFirst","parsed"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/mcp/mcp.service.ts"],"sourcesContent":["/**\n * MCP Service\n *\n * Business logic for MCP server management.\n * All queries are scoped to the user's active organization.\n * System records (organizationId=null, including builtin) are visible to all.\n */\n\nimport {\n ExternalMcpServer,\n RemoteHttpMcpServer,\n RemoteSseMcpServer,\n} from '@dxheroes/local-mcp-core';\nimport { ConflictException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';\n\n/** Sentinel value for unauthenticated MCP access — can only see system servers */\nconst UNAUTHENTICATED_ID = '__unauthenticated__';\n\nimport { PrismaService } from '../database/prisma.service.js';\nimport { DebugService } from '../debug/debug.service.js';\nimport type { CreateMcpServerDto } from './dto/create-mcp-server.dto.js';\nimport type { UpdateMcpServerDto } from './dto/update-mcp-server.dto.js';\nimport { MCP_PRESETS } from './mcp-presets.js';\nimport { McpRegistry } from './mcp-registry.js';\n\n@Injectable()\nexport class McpService {\n constructor(\n private readonly prisma: PrismaService,\n private readonly registry: McpRegistry,\n private readonly debugService: DebugService\n ) {}\n\n private isAnonymous(userId: string): boolean {\n return userId === UNAUTHENTICATED_ID;\n }\n\n private async assertAccess(serverId: string, userId: string, orgId?: string): Promise<void> {\n if (this.isAnonymous(userId)) return;\n\n const server = await this.prisma.mcpServer.findUnique({\n where: { id: serverId },\n select: { userId: true, organizationId: true },\n });\n if (!server) throw new NotFoundException(`MCP server ${serverId} not found`);\n\n // System record (no org) — accessible to all\n if (!server.organizationId) return;\n\n // Must belong to the active org\n if (orgId && server.organizationId === orgId) return;\n\n throw new ForbiddenException('You do not have access to this MCP server');\n }\n\n private async assertOwnership(serverId: string, userId: string, orgId?: string): Promise<void> {\n if (this.isAnonymous(userId)) return;\n\n const server = await this.prisma.mcpServer.findUnique({\n where: { id: serverId },\n select: { userId: true, organizationId: true },\n });\n if (!server) throw new NotFoundException(`MCP server ${serverId} not found`);\n if (!server.organizationId) return; // system record\n\n if (orgId && server.organizationId === orgId) return;\n\n throw new ForbiddenException('You do not own this MCP server');\n }\n\n /**\n * Get all MCP servers visible in the active org (org servers + system servers)\n */\n async findAll(userId: string, orgId?: string) {\n const include = { profiles: { include: { profile: true as const } } };\n const orderBy = { name: 'asc' as const };\n\n const servers = this.isAnonymous(userId)\n ? await this.prisma.mcpServer.findMany({ include, orderBy })\n : await this.prisma.mcpServer.findMany({\n where: {\n OR: [\n { organizationId: orgId }, // org servers\n { organizationId: null }, // system servers\n ],\n },\n include,\n orderBy,\n });\n\n // Enrich with metadata from registry for builtin servers\n return servers.map((server) => {\n const builtinId = this.getBuiltinId(server.config);\n return builtinId && this.registry.has(builtinId)\n ? { ...server, metadata: this.registry.get(builtinId)?.metadata }\n : server;\n });\n }\n\n /**\n * Get a specific MCP server\n */\n async findById(id: string, userId?: string, orgId?: string) {\n if (userId) {\n await this.assertAccess(id, userId, orgId);\n }\n\n const server = await this.prisma.mcpServer.findUnique({\n where: { id },\n include: {\n profiles: {\n include: {\n profile: true,\n tools: true,\n },\n },\n oauthToken: true,\n toolsCache: true,\n },\n });\n\n if (!server) {\n throw new NotFoundException(`MCP server ${id} not found`);\n }\n\n // Enrich with metadata from registry for builtin servers\n const builtinId = this.getBuiltinId(server.config);\n\n return builtinId && this.registry.has(builtinId)\n ? { ...server, metadata: this.registry.get(builtinId)?.metadata }\n : server;\n }\n\n /**\n * Create a new MCP server in the active org\n */\n async create(dto: CreateMcpServerDto, userId: string, orgId?: string) {\n return this.prisma.mcpServer.create({\n data: {\n name: dto.name,\n type: dto.type,\n config: JSON.stringify(dto.config || {}),\n apiKeyConfig: dto.apiKeyConfig ? JSON.stringify(dto.apiKeyConfig) : null,\n oauthConfig: dto.oauthConfig ? JSON.stringify(dto.oauthConfig) : null,\n userId: this.isAnonymous(userId) ? null : userId,\n organizationId: orgId ?? null,\n },\n });\n }\n\n /**\n * Update an MCP server\n */\n async update(id: string, dto: UpdateMcpServerDto, userId: string, orgId?: string) {\n await this.assertOwnership(id, userId, orgId);\n\n const server = await this.prisma.mcpServer.findUnique({ where: { id } });\n if (!server) {\n throw new NotFoundException(`MCP server ${id} not found`);\n }\n\n return this.prisma.mcpServer.update({\n where: { id },\n data: {\n name: dto.name,\n type: dto.type,\n config:\n dto.config !== undefined\n ? typeof dto.config === 'string'\n ? dto.config\n : JSON.stringify(dto.config)\n : undefined,\n apiKeyConfig: dto.apiKeyConfig !== undefined ? JSON.stringify(dto.apiKeyConfig) : undefined,\n oauthConfig: dto.oauthConfig !== undefined ? JSON.stringify(dto.oauthConfig) : undefined,\n },\n });\n }\n\n /**\n * Delete an MCP server\n */\n async delete(id: string, userId: string, orgId?: string) {\n await this.assertOwnership(id, userId, orgId);\n\n const server = await this.prisma.mcpServer.findUnique({ where: { id } });\n if (!server) {\n throw new NotFoundException(`MCP server ${id} not found`);\n }\n\n await this.prisma.mcpServer.delete({ where: { id } });\n }\n\n /**\n * Get tools from an MCP server\n */\n async getTools(id: string, userId?: string, orgId?: string) {\n if (userId) {\n await this.assertAccess(id, userId, orgId);\n }\n\n const startTime = Date.now();\n\n // Create debug log entry\n const log = await this.debugService.createLog({\n mcpServerId: id,\n requestType: 'tools/list',\n requestPayload: JSON.stringify({ method: 'tools/list', serverId: id }),\n status: 'pending',\n });\n\n try {\n const result = await this.getToolsInternal(id);\n\n // Update debug log with success\n await this.debugService.updateLog(log.id, {\n responsePayload: JSON.stringify(result),\n status: 'success',\n durationMs: Date.now() - startTime,\n });\n\n return result;\n } catch (error) {\n // Update debug log with error\n await this.debugService.updateLog(log.id, {\n status: 'error',\n errorMessage: error instanceof Error ? error.message : 'Unknown error',\n durationMs: Date.now() - startTime,\n });\n throw error;\n }\n }\n\n /**\n * Internal method to get tools from an MCP server\n */\n private async getToolsInternal(id: string) {\n const server = await this.findById(id);\n const builtinId = this.getBuiltinId(server.config);\n\n // For builtin servers, get tools from the registry\n if (builtinId && this.registry.has(builtinId)) {\n const pkg = this.registry.get(builtinId);\n if (pkg) {\n // Get API key config if set\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n // Create server instance and list tools\n const instance = pkg.createServer(apiKeyConfig);\n await instance.initialize();\n const tools = await instance.listTools();\n return { tools };\n }\n }\n\n // For remote_http servers, connect and fetch tools\n if (server.type === 'remote_http') {\n const config = this.parseConfig(server.config) as { url: string; headers?: Record<string, string> };\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n const remoteServer = new RemoteHttpMcpServer(\n { url: config.url, transport: 'http', headers: config.headers },\n null,\n apiKeyConfig\n );\n await remoteServer.initialize();\n const tools = await remoteServer.listTools();\n return { tools };\n }\n\n // For remote_sse servers, connect and fetch tools\n if (server.type === 'remote_sse') {\n const config = this.parseConfig(server.config) as { url: string; headers?: Record<string, string> };\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n const remoteServer = new RemoteSseMcpServer(\n { url: config.url, transport: 'sse', headers: config.headers },\n null,\n apiKeyConfig\n );\n await remoteServer.initialize();\n const tools = await remoteServer.listTools();\n return { tools };\n }\n\n // For external (NPX/stdio) servers, spawn and fetch tools\n if (server.type === 'external') {\n const config = this.parseConfig(server.config) as {\n command: string;\n args?: string[];\n env?: Record<string, string>;\n workingDirectory?: string;\n autoRestart?: boolean;\n maxRestartAttempts?: number;\n startupTimeout?: number;\n shutdownTimeout?: number;\n };\n\n const externalServer = new ExternalMcpServer(config);\n try {\n await externalServer.initialize();\n const tools = await externalServer.listTools();\n return { tools };\n } finally {\n // Shutdown after fetching tools\n await externalServer.shutdown();\n }\n }\n\n // For cached tools from external servers (fallback)\n const tools = await this.prisma.mcpServerToolsCache.findMany({\n where: { mcpServerId: id },\n });\n return { tools };\n }\n\n /**\n * Get MCP server status with real validation\n */\n async getStatus(id: string, userId?: string, orgId?: string) {\n if (userId) {\n await this.assertAccess(id, userId, orgId);\n }\n\n const startTime = Date.now();\n\n // Create debug log entry\n const log = await this.debugService.createLog({\n mcpServerId: id,\n requestType: 'status/check',\n requestPayload: JSON.stringify({ method: 'status/check', serverId: id }),\n status: 'pending',\n });\n\n try {\n const result = await this.getStatusInternal(id);\n\n // Update debug log with success or error based on status\n await this.debugService.updateLog(log.id, {\n responsePayload: JSON.stringify(result),\n status: result.status === 'error' ? 'error' : 'success',\n errorMessage: result.error,\n durationMs: Date.now() - startTime,\n });\n\n return result;\n } catch (error) {\n // Update debug log with error\n await this.debugService.updateLog(log.id, {\n status: 'error',\n errorMessage: error instanceof Error ? error.message : 'Unknown error',\n durationMs: Date.now() - startTime,\n });\n throw error;\n }\n }\n\n /**\n * Internal method to get MCP server status with real validation\n */\n private async getStatusInternal(id: string) {\n const server = await this.findById(id);\n const builtinId = this.getBuiltinId(server.config);\n\n // Check if it's a builtin server\n const isBuiltin = builtinId && this.registry.has(builtinId);\n\n // Check API key config\n const hasApiKey = !!server.apiKeyConfig;\n\n // Check OAuth token\n const hasOAuth = !!server.oauthToken;\n\n // Get metadata\n const metadata = isBuiltin ? this.registry.get(builtinId)?.metadata : null;\n const requiresApiKey = metadata?.requiresApiKey ?? false;\n const requiresOAuth = metadata?.requiresOAuth ?? false;\n\n // Default status\n let status: 'connected' | 'error' | 'unknown' = 'unknown';\n let validationError: string | undefined;\n let validationDetails: string | undefined;\n\n // For builtin servers with API key, actually validate\n if (isBuiltin && hasApiKey) {\n const pkg = this.registry.get(builtinId);\n if (pkg) {\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n try {\n const instance = pkg.createServer(apiKeyConfig);\n const validation = await instance.validate();\n status = validation.valid ? 'connected' : 'error';\n validationError = validation.error;\n validationDetails = validation.valid\n ? 'API key validated successfully'\n : `Validation failed: ${validation.error}`;\n } catch (error) {\n status = 'error';\n validationError = error instanceof Error ? error.message : 'Unknown error';\n validationDetails = `Connection test failed: ${validationError}`;\n }\n }\n } else if (!hasApiKey && requiresApiKey) {\n status = 'error';\n validationError = 'API key required';\n validationDetails = 'This server requires an API key to function';\n } else if (isBuiltin && !requiresApiKey) {\n status = 'connected';\n validationDetails = 'Server ready (no API key required)';\n }\n\n // For remote_http servers, validate by connecting\n let oauthRequired = false;\n if (server.type === 'remote_http' && status === 'unknown') {\n const config = this.parseConfig(server.config) as { url: string; headers?: Record<string, string> };\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n // Get OAuth token if available, mapping Prisma types to core OAuthToken\n const oauthToken = server.oauthToken\n ? {\n id: server.oauthToken.id,\n mcpServerId: server.oauthToken.mcpServerId,\n accessToken: server.oauthToken.accessToken,\n tokenType: server.oauthToken.tokenType,\n refreshToken: server.oauthToken.refreshToken ?? undefined,\n scope: server.oauthToken.scope ?? undefined,\n expiresAt: server.oauthToken.expiresAt?.getTime(),\n createdAt: server.oauthToken.createdAt.getTime(),\n updatedAt: server.oauthToken.updatedAt.getTime(),\n }\n : null;\n\n try {\n const remoteServer = new RemoteHttpMcpServer(\n { url: config.url, transport: 'http', headers: config.headers },\n oauthToken,\n apiKeyConfig\n );\n await remoteServer.initialize();\n const tools = await remoteServer.listTools();\n status = 'connected';\n validationDetails = `Connected successfully. ${tools.length} tools available.`;\n } catch (error) {\n status = 'error';\n validationError = error instanceof Error ? error.message : 'Unknown error';\n if (validationError.includes('OAUTH_REQUIRED')) {\n oauthRequired = true;\n validationDetails = 'OAuth authentication required. Click \"Login with OAuth\" to authorize.';\n } else {\n validationDetails = `Connection failed: ${validationError}`;\n }\n }\n }\n\n // For remote_sse servers, validate by connecting\n if (server.type === 'remote_sse' && status === 'unknown') {\n const config = this.parseConfig(server.config) as { url: string; headers?: Record<string, string> };\n const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;\n\n // Get OAuth token if available, mapping Prisma types to core OAuthToken\n const oauthToken = server.oauthToken\n ? {\n id: server.oauthToken.id,\n mcpServerId: server.oauthToken.mcpServerId,\n accessToken: server.oauthToken.accessToken,\n tokenType: server.oauthToken.tokenType,\n refreshToken: server.oauthToken.refreshToken ?? undefined,\n scope: server.oauthToken.scope ?? undefined,\n expiresAt: server.oauthToken.expiresAt?.getTime(),\n createdAt: server.oauthToken.createdAt.getTime(),\n updatedAt: server.oauthToken.updatedAt.getTime(),\n }\n : null;\n\n try {\n const remoteServer = new RemoteSseMcpServer(\n { url: config.url, transport: 'sse', headers: config.headers },\n oauthToken,\n apiKeyConfig\n );\n await remoteServer.initialize();\n const tools = await remoteServer.listTools();\n status = 'connected';\n validationDetails = `Connected successfully (SSE). ${tools.length} tools available.`;\n } catch (error) {\n status = 'error';\n validationError = error instanceof Error ? error.message : 'Unknown error';\n if (validationError.includes('OAUTH_REQUIRED')) {\n oauthRequired = true;\n validationDetails = 'OAuth authentication required. Click \"Login with OAuth\" to authorize.';\n } else {\n validationDetails = `Connection failed: ${validationError}`;\n }\n }\n }\n\n // For external (NPX/stdio) servers, validate by spawning and checking\n if (server.type === 'external' && status === 'unknown') {\n const config = this.parseConfig(server.config) as {\n command: string;\n args?: string[];\n env?: Record<string, string>;\n workingDirectory?: string;\n };\n\n if (!config.command) {\n status = 'error';\n validationError = 'Command is required for external MCP servers';\n validationDetails = 'Missing command configuration';\n } else {\n try {\n const externalServer = new ExternalMcpServer(config);\n await externalServer.initialize();\n const tools = await externalServer.listTools();\n status = 'connected';\n validationDetails = `Connected successfully (stdio). ${tools.length} tools available.`;\n await externalServer.shutdown();\n } catch (error) {\n status = 'error';\n validationError = error instanceof Error ? error.message : 'Unknown error';\n validationDetails = `Connection failed: ${validationError}`;\n }\n }\n }\n\n const isReady = status === 'connected';\n\n return {\n id: server.id,\n name: server.name,\n type: server.type,\n isBuiltin,\n hasApiKey,\n hasOAuth,\n requiresApiKey,\n requiresOAuth,\n oauthRequired,\n isReady,\n status,\n error: validationError,\n details: validationDetails,\n validatedAt: new Date().toISOString(),\n };\n }\n\n /**\n * Add a preset MCP server to the user's organization\n */\n async addPreset(presetId: string, userId: string, orgId: string) {\n const preset = MCP_PRESETS.find((p) => p.id === presetId);\n if (!preset) {\n throw new NotFoundException(`Preset \"${presetId}\" not found`);\n }\n\n // Check if a server with this name already exists in the org\n const existing = await this.prisma.mcpServer.findFirst({\n where: {\n name: preset.name,\n organizationId: orgId,\n },\n });\n\n if (existing) {\n throw new ConflictException(\n `MCP server \"${preset.name}\" already exists in your organization`\n );\n }\n\n return this.prisma.mcpServer.create({\n data: {\n name: preset.name,\n type: preset.type,\n config: JSON.stringify(preset.config),\n userId,\n organizationId: orgId,\n },\n });\n }\n\n /**\n * Extract builtinId from config (handles JSON string parsing)\n */\n private getBuiltinId(config: unknown): string | null {\n const parsed = this.parseConfig(config);\n const builtinId = parsed?.builtinId;\n\n if (typeof builtinId === 'string') {\n return builtinId;\n }\n return null;\n }\n\n private parseConfig(config: unknown): Record<string, unknown> | null {\n if (typeof config === 'string') {\n try {\n return JSON.parse(config);\n } catch {\n return null;\n }\n }\n return config as Record<string, unknown> | null;\n }\n}\n"],"names":["ExternalMcpServer","RemoteHttpMcpServer","RemoteSseMcpServer","ConflictException","ForbiddenException","Injectable","NotFoundException","UNAUTHENTICATED_ID","PrismaService","DebugService","MCP_PRESETS","McpRegistry","McpService","prisma","registry","debugService","isAnonymous","userId","assertAccess","serverId","orgId","server","mcpServer","findUnique","where","id","select","organizationId","assertOwnership","findAll","include","profiles","profile","orderBy","name","servers","findMany","OR","map","builtinId","getBuiltinId","config","has","metadata","get","findById","tools","oauthToken","toolsCache","create","dto","data","type","JSON","stringify","apiKeyConfig","oauthConfig","update","undefined","delete","getTools","startTime","Date","now","log","createLog","mcpServerId","requestType","requestPayload","method","status","result","getToolsInternal","updateLog","responsePayload","durationMs","error","errorMessage","Error","message","pkg","parse","instance","createServer","initialize","listTools","parseConfig","remoteServer","url","transport","headers","externalServer","shutdown","mcpServerToolsCache","getStatus","getStatusInternal","isBuiltin","hasApiKey","hasOAuth","requiresApiKey","requiresOAuth","validationError","validationDetails","validation","validate","valid","oauthRequired","accessToken","tokenType","refreshToken","scope","expiresAt","getTime","createdAt","updatedAt","length","includes","command","isReady","details","validatedAt","toISOString","addPreset","presetId","preset","find","p","existing","findFirst","parsed"],"mappings":"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"}
|
|
@@ -16,12 +16,61 @@ function _ts_param(paramIndex, decorator) {
|
|
|
16
16
|
* OAuth Controller
|
|
17
17
|
*
|
|
18
18
|
* REST API endpoints for OAuth token management.
|
|
19
|
-
*/ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post } from "@nestjs/common";
|
|
19
|
+
*/ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Logger, Param, Post, Query, Req, Res } from "@nestjs/common";
|
|
20
20
|
import { SkipOrgCheck } from "../auth/decorators/skip-org-check.decorator.js";
|
|
21
21
|
import { OAuthService } from "./oauth.service.js";
|
|
22
22
|
export class OAuthController {
|
|
23
23
|
constructor(oauthService){
|
|
24
24
|
this.oauthService = oauthService;
|
|
25
|
+
this.logger = new Logger(OAuthController.name);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start OAuth auto-discovery flow for an MCP server.
|
|
29
|
+
* Discovers OAuth endpoints via RFC 9728/8414, performs DCR if needed,
|
|
30
|
+
* and redirects the browser to the authorization server.
|
|
31
|
+
*/ async authorize(serverId, req, res) {
|
|
32
|
+
try {
|
|
33
|
+
const callbackUrl = `${req.protocol}://${req.get('host')}/api/oauth/callback`;
|
|
34
|
+
const authorizationUrl = await this.oauthService.discoverAndAuthorize(serverId, callbackUrl);
|
|
35
|
+
res.redirect(authorizationUrl);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
38
|
+
this.logger.error(`OAuth authorize failed for ${serverId}: ${message}`);
|
|
39
|
+
res.status(400).send(this.renderCallbackPage(false, message));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* OAuth callback endpoint. Receives authorization code, exchanges for tokens,
|
|
44
|
+
* and returns an HTML page that notifies the opener window.
|
|
45
|
+
*/ async callback(code, state, req, res) {
|
|
46
|
+
if (!code || !state) {
|
|
47
|
+
res.status(400).send(this.renderCallbackPage(false, 'Missing code or state parameter'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const serverId = state;
|
|
51
|
+
const callbackUrl = `${req.protocol}://${req.get('host')}/api/oauth/callback`;
|
|
52
|
+
const result = await this.oauthService.handleCallback(serverId, code, callbackUrl);
|
|
53
|
+
res.status(200).send(this.renderCallbackPage(result.success, result.error));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Render an HTML page that posts a message to the opener window and closes itself.
|
|
57
|
+
*/ renderCallbackPage(success, error) {
|
|
58
|
+
const message = JSON.stringify({
|
|
59
|
+
type: 'oauth-callback',
|
|
60
|
+
success,
|
|
61
|
+
error: error || null
|
|
62
|
+
});
|
|
63
|
+
return `<!DOCTYPE html>
|
|
64
|
+
<html><head><title>OAuth ${success ? 'Success' : 'Error'}</title></head>
|
|
65
|
+
<body>
|
|
66
|
+
<p>${success ? 'Authorization successful! This window will close.' : `Error: ${error}`}</p>
|
|
67
|
+
<script>
|
|
68
|
+
if (window.opener) {
|
|
69
|
+
window.opener.postMessage(${message}, '*');
|
|
70
|
+
}
|
|
71
|
+
setTimeout(function() { window.close(); }, 2000);
|
|
72
|
+
</script>
|
|
73
|
+
</body></html>`;
|
|
25
74
|
}
|
|
26
75
|
/**
|
|
27
76
|
* Start OAuth flow for an MCP server
|
|
@@ -57,6 +106,34 @@ export class OAuthController {
|
|
|
57
106
|
await this.oauthService.deleteToken(serverId);
|
|
58
107
|
}
|
|
59
108
|
}
|
|
109
|
+
_ts_decorate([
|
|
110
|
+
Get('authorize/:serverId'),
|
|
111
|
+
_ts_param(0, Param('serverId')),
|
|
112
|
+
_ts_param(1, Req()),
|
|
113
|
+
_ts_param(2, Res()),
|
|
114
|
+
_ts_metadata("design:type", Function),
|
|
115
|
+
_ts_metadata("design:paramtypes", [
|
|
116
|
+
String,
|
|
117
|
+
typeof Request === "undefined" ? Object : Request,
|
|
118
|
+
typeof Response === "undefined" ? Object : Response
|
|
119
|
+
]),
|
|
120
|
+
_ts_metadata("design:returntype", Promise)
|
|
121
|
+
], OAuthController.prototype, "authorize", null);
|
|
122
|
+
_ts_decorate([
|
|
123
|
+
Get('callback'),
|
|
124
|
+
_ts_param(0, Query('code')),
|
|
125
|
+
_ts_param(1, Query('state')),
|
|
126
|
+
_ts_param(2, Req()),
|
|
127
|
+
_ts_param(3, Res()),
|
|
128
|
+
_ts_metadata("design:type", Function),
|
|
129
|
+
_ts_metadata("design:paramtypes", [
|
|
130
|
+
String,
|
|
131
|
+
String,
|
|
132
|
+
typeof Request === "undefined" ? Object : Request,
|
|
133
|
+
typeof Response === "undefined" ? Object : Response
|
|
134
|
+
]),
|
|
135
|
+
_ts_metadata("design:returntype", Promise)
|
|
136
|
+
], OAuthController.prototype, "callback", null);
|
|
60
137
|
_ts_decorate([
|
|
61
138
|
Post('start'),
|
|
62
139
|
_ts_param(0, Body()),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/oauth/oauth.controller.ts"],"sourcesContent":["/**\n * OAuth Controller\n *\n * REST API endpoints for OAuth token management.\n */\n\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/oauth/oauth.controller.ts"],"sourcesContent":["/**\n * OAuth Controller\n *\n * REST API endpoints for OAuth token management.\n */\n\nimport {\n Body,\n Controller,\n Delete,\n Get,\n HttpCode,\n HttpStatus,\n Logger,\n Param,\n Post,\n Query,\n Req,\n Res,\n} from '@nestjs/common';\nimport type { Request, Response } from 'express';\nimport { SkipOrgCheck } from '../auth/decorators/skip-org-check.decorator.js';\nimport { OAuthService } from './oauth.service.js';\n\ninterface StartOAuthDto {\n mcpServerId: string;\n authorizationServerUrl: string;\n clientId: string;\n scopes?: string[];\n redirectUri: string;\n}\n\ninterface CompleteOAuthDto {\n mcpServerId: string;\n code: string;\n codeVerifier: string;\n tokenEndpoint: string;\n clientId: string;\n redirectUri: string;\n}\n\ninterface StoreTokenDto {\n accessToken: string;\n refreshToken?: string | null;\n tokenType?: string;\n scope?: string | null;\n expiresIn?: number;\n}\n\n@SkipOrgCheck()\n@Controller('oauth')\nexport class OAuthController {\n private readonly logger = new Logger(OAuthController.name);\n\n constructor(private readonly oauthService: OAuthService) {}\n\n /**\n * Start OAuth auto-discovery flow for an MCP server.\n * Discovers OAuth endpoints via RFC 9728/8414, performs DCR if needed,\n * and redirects the browser to the authorization server.\n */\n @Get('authorize/:serverId')\n async authorize(\n @Param('serverId') serverId: string,\n @Req() req: Request,\n @Res() res: Response\n ) {\n try {\n const callbackUrl = `${req.protocol}://${req.get('host')}/api/oauth/callback`;\n const authorizationUrl = await this.oauthService.discoverAndAuthorize(serverId, callbackUrl);\n res.redirect(authorizationUrl);\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n this.logger.error(`OAuth authorize failed for ${serverId}: ${message}`);\n res.status(400).send(this.renderCallbackPage(false, message));\n }\n }\n\n /**\n * OAuth callback endpoint. Receives authorization code, exchanges for tokens,\n * and returns an HTML page that notifies the opener window.\n */\n @Get('callback')\n async callback(\n @Query('code') code: string,\n @Query('state') state: string,\n @Req() req: Request,\n @Res() res: Response\n ) {\n if (!code || !state) {\n res.status(400).send(this.renderCallbackPage(false, 'Missing code or state parameter'));\n return;\n }\n\n const serverId = state;\n const callbackUrl = `${req.protocol}://${req.get('host')}/api/oauth/callback`;\n\n const result = await this.oauthService.handleCallback(serverId, code, callbackUrl);\n res.status(200).send(this.renderCallbackPage(result.success, result.error));\n }\n\n /**\n * Render an HTML page that posts a message to the opener window and closes itself.\n */\n private renderCallbackPage(success: boolean, error?: string): string {\n const message = JSON.stringify({\n type: 'oauth-callback',\n success,\n error: error || null,\n });\n return `<!DOCTYPE html>\n<html><head><title>OAuth ${success ? 'Success' : 'Error'}</title></head>\n<body>\n<p>${success ? 'Authorization successful! This window will close.' : `Error: ${error}`}</p>\n<script>\n if (window.opener) {\n window.opener.postMessage(${message}, '*');\n }\n setTimeout(function() { window.close(); }, 2000);\n</script>\n</body></html>`;\n }\n\n /**\n * Start OAuth flow for an MCP server\n */\n @Post('start')\n async startOAuthFlow(@Body() dto: StartOAuthDto) {\n return this.oauthService.startOAuthFlow(dto);\n }\n\n /**\n * Complete OAuth flow with authorization code\n */\n @Post('complete')\n async completeOAuthFlow(@Body() dto: CompleteOAuthDto) {\n return this.oauthService.completeOAuthFlow(dto);\n }\n\n /**\n * Manually store a token for an MCP server\n */\n @Post('servers/:serverId/token')\n @HttpCode(HttpStatus.CREATED)\n async storeToken(@Param('serverId') serverId: string, @Body() dto: StoreTokenDto) {\n const expiresAt = dto.expiresIn ? new Date(Date.now() + dto.expiresIn * 1000) : null;\n\n return this.oauthService.storeToken({\n mcpServerId: serverId,\n accessToken: dto.accessToken,\n refreshToken: dto.refreshToken,\n tokenType: dto.tokenType || 'Bearer',\n scope: dto.scope,\n expiresAt,\n });\n }\n\n /**\n * Get OAuth token for an MCP server\n */\n @Get('servers/:serverId/token')\n async getToken(@Param('serverId') serverId: string) {\n return this.oauthService.getToken(serverId);\n }\n\n /**\n * Delete OAuth token for an MCP server\n */\n @Delete('servers/:serverId/token')\n @HttpCode(HttpStatus.NO_CONTENT)\n async deleteToken(@Param('serverId') serverId: string) {\n await this.oauthService.deleteToken(serverId);\n }\n}\n"],"names":["Body","Controller","Delete","Get","HttpCode","HttpStatus","Logger","Param","Post","Query","Req","Res","SkipOrgCheck","OAuthService","OAuthController","oauthService","logger","name","authorize","serverId","req","res","callbackUrl","protocol","get","authorizationUrl","discoverAndAuthorize","redirect","error","message","Error","status","send","renderCallbackPage","callback","code","state","result","handleCallback","success","JSON","stringify","type","startOAuthFlow","dto","completeOAuthFlow","storeToken","expiresAt","expiresIn","Date","now","mcpServerId","accessToken","refreshToken","tokenType","scope","getToken","deleteToken","CREATED","NO_CONTENT"],"mappings":"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"}
|
|
@@ -11,13 +11,16 @@ function _ts_metadata(k, v) {
|
|
|
11
11
|
* OAuth Service
|
|
12
12
|
*
|
|
13
13
|
* Manages OAuth tokens for MCP servers.
|
|
14
|
-
*/ import {
|
|
14
|
+
*/ import { OAuthDiscoveryService } from "@dxheroes/local-mcp-core";
|
|
15
|
+
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
|
15
16
|
import { PrismaService } from "../database/prisma.service.js";
|
|
16
17
|
export class OAuthService {
|
|
17
18
|
constructor(prisma){
|
|
18
19
|
this.prisma = prisma;
|
|
20
|
+
this.logger = new Logger(OAuthService.name);
|
|
19
21
|
// In-memory storage for PKCE verifiers (in production, use Redis or similar)
|
|
20
22
|
this.pkceVerifiers = new Map();
|
|
23
|
+
this.discoveryService = new OAuthDiscoveryService();
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
26
|
* Generate PKCE challenge and store verifier
|
|
@@ -151,6 +154,199 @@ export class OAuthService {
|
|
|
151
154
|
});
|
|
152
155
|
}
|
|
153
156
|
/**
|
|
157
|
+
* Discover OAuth configuration and build an authorization URL for a server.
|
|
158
|
+
* Uses RFC 9728 / 8414 / 7591 auto-discovery and optional DCR.
|
|
159
|
+
* Returns an authorization URL the browser should be redirected to.
|
|
160
|
+
*/ async discoverAndAuthorize(serverId, callbackUrl) {
|
|
161
|
+
const server = await this.prisma.mcpServer.findUnique({
|
|
162
|
+
where: {
|
|
163
|
+
id: serverId
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
if (!server) {
|
|
167
|
+
throw new NotFoundException(`MCP server ${serverId} not found`);
|
|
168
|
+
}
|
|
169
|
+
// Parse server URL from config
|
|
170
|
+
const config = typeof server.config === 'string' ? JSON.parse(server.config) : server.config;
|
|
171
|
+
const serverUrl = config?.url;
|
|
172
|
+
if (!serverUrl) {
|
|
173
|
+
throw new BadRequestException('Server has no URL configured');
|
|
174
|
+
}
|
|
175
|
+
// Parse existing oauthConfig if any (may have manual clientId)
|
|
176
|
+
const existingOAuthConfig = server.oauthConfig ? typeof server.oauthConfig === 'string' ? JSON.parse(server.oauthConfig) : server.oauthConfig : null;
|
|
177
|
+
// Step 1: Discover OAuth endpoints via RFC 9728 + 8414
|
|
178
|
+
this.logger.log(`Discovering OAuth for server ${serverId} at ${serverUrl}`);
|
|
179
|
+
const discovery = await this.discoveryService.discoverFromServerUrl(serverUrl);
|
|
180
|
+
// Step 2: Get or register client
|
|
181
|
+
let clientId;
|
|
182
|
+
if (existingOAuthConfig?.clientId) {
|
|
183
|
+
// Use manually configured clientId
|
|
184
|
+
clientId = existingOAuthConfig.clientId;
|
|
185
|
+
} else {
|
|
186
|
+
// Check for existing DCR registration
|
|
187
|
+
const existingReg = await this.prisma.oAuthClientRegistration.findUnique({
|
|
188
|
+
where: {
|
|
189
|
+
mcpServerId_authorizationServerUrl: {
|
|
190
|
+
mcpServerId: serverId,
|
|
191
|
+
authorizationServerUrl: discovery.authorizationServerUrl
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
if (existingReg) {
|
|
196
|
+
// Delete stale registration — redirect_uri may have changed
|
|
197
|
+
await this.prisma.oAuthClientRegistration.delete({
|
|
198
|
+
where: {
|
|
199
|
+
id: existingReg.id
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
this.logger.log(`Deleted stale DCR registration for ${serverId}, will re-register`);
|
|
203
|
+
}
|
|
204
|
+
if (discovery.registrationEndpoint) {
|
|
205
|
+
// Perform Dynamic Client Registration (RFC 7591)
|
|
206
|
+
this.logger.log(`Performing DCR at ${discovery.registrationEndpoint}`);
|
|
207
|
+
const registration = await this.discoveryService.registerClient(discovery.registrationEndpoint, callbackUrl, discovery.scopes);
|
|
208
|
+
clientId = registration.clientId;
|
|
209
|
+
// Store registration
|
|
210
|
+
await this.prisma.oAuthClientRegistration.create({
|
|
211
|
+
data: {
|
|
212
|
+
mcpServerId: serverId,
|
|
213
|
+
authorizationServerUrl: discovery.authorizationServerUrl,
|
|
214
|
+
clientId: registration.clientId,
|
|
215
|
+
clientSecret: registration.clientSecret,
|
|
216
|
+
registrationAccessToken: registration.registrationAccessToken
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
throw new BadRequestException('No clientId configured and server does not support Dynamic Client Registration. ' + 'Please configure a clientId manually in the OAuth settings.');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Step 3: Store discovery result in oauthConfig on the server
|
|
224
|
+
await this.prisma.mcpServer.update({
|
|
225
|
+
where: {
|
|
226
|
+
id: serverId
|
|
227
|
+
},
|
|
228
|
+
data: {
|
|
229
|
+
oauthConfig: JSON.stringify({
|
|
230
|
+
authorizationServerUrl: discovery.authorizationServerUrl,
|
|
231
|
+
authorizationEndpoint: discovery.authorizationEndpoint,
|
|
232
|
+
tokenEndpoint: discovery.tokenEndpoint,
|
|
233
|
+
registrationEndpoint: discovery.registrationEndpoint,
|
|
234
|
+
resource: discovery.resource,
|
|
235
|
+
scopes: discovery.scopes,
|
|
236
|
+
clientId,
|
|
237
|
+
requiresOAuth: true
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
// Step 4: Generate PKCE
|
|
242
|
+
const codeVerifier = this.generateCodeVerifier();
|
|
243
|
+
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
|
|
244
|
+
// Store verifier (expires in 10 minutes)
|
|
245
|
+
this.pkceVerifiers.set(serverId, {
|
|
246
|
+
verifier: codeVerifier,
|
|
247
|
+
expiresAt: Date.now() + 10 * 60 * 1000
|
|
248
|
+
});
|
|
249
|
+
// Step 5: Build authorization URL
|
|
250
|
+
const params = new URLSearchParams({
|
|
251
|
+
response_type: 'code',
|
|
252
|
+
client_id: clientId,
|
|
253
|
+
redirect_uri: callbackUrl,
|
|
254
|
+
code_challenge: codeChallenge,
|
|
255
|
+
code_challenge_method: 'S256',
|
|
256
|
+
state: serverId
|
|
257
|
+
});
|
|
258
|
+
if (discovery.scopes.length > 0) {
|
|
259
|
+
params.set('scope', discovery.scopes.join(' '));
|
|
260
|
+
}
|
|
261
|
+
if (discovery.resource) {
|
|
262
|
+
params.set('resource', discovery.resource);
|
|
263
|
+
}
|
|
264
|
+
return `${discovery.authorizationEndpoint}?${params.toString()}`;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Handle OAuth callback: exchange authorization code for tokens.
|
|
268
|
+
*/ async handleCallback(serverId, code, callbackUrl) {
|
|
269
|
+
const server = await this.prisma.mcpServer.findUnique({
|
|
270
|
+
where: {
|
|
271
|
+
id: serverId
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
if (!server) {
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
error: `MCP server ${serverId} not found`
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// Retrieve PKCE verifier
|
|
281
|
+
const pkceEntry = this.pkceVerifiers.get(serverId);
|
|
282
|
+
if (!pkceEntry) {
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
error: 'PKCE verifier not found or expired'
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (pkceEntry.expiresAt < Date.now()) {
|
|
289
|
+
this.pkceVerifiers.delete(serverId);
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
error: 'PKCE verifier expired'
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const codeVerifier = pkceEntry.verifier;
|
|
296
|
+
this.pkceVerifiers.delete(serverId);
|
|
297
|
+
// Get oauthConfig for token endpoint and clientId
|
|
298
|
+
const oauthConfig = server.oauthConfig ? typeof server.oauthConfig === 'string' ? JSON.parse(server.oauthConfig) : server.oauthConfig : null;
|
|
299
|
+
if (!oauthConfig?.tokenEndpoint || !oauthConfig?.clientId) {
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
error: 'OAuth configuration incomplete (missing tokenEndpoint or clientId)'
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
// Exchange code for tokens
|
|
307
|
+
const response = await fetch(oauthConfig.tokenEndpoint, {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
headers: {
|
|
310
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
311
|
+
},
|
|
312
|
+
body: new URLSearchParams({
|
|
313
|
+
grant_type: 'authorization_code',
|
|
314
|
+
code,
|
|
315
|
+
redirect_uri: callbackUrl,
|
|
316
|
+
client_id: oauthConfig.clientId,
|
|
317
|
+
code_verifier: codeVerifier
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
const errorText = await response.text();
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
error: `Token exchange failed: ${errorText}`
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const tokenData = await response.json();
|
|
328
|
+
const expiresAt = tokenData.expires_in ? new Date(Date.now() + tokenData.expires_in * 1000) : null;
|
|
329
|
+
// Store token
|
|
330
|
+
await this.storeToken({
|
|
331
|
+
mcpServerId: serverId,
|
|
332
|
+
accessToken: tokenData.access_token,
|
|
333
|
+
refreshToken: tokenData.refresh_token,
|
|
334
|
+
tokenType: tokenData.token_type || 'Bearer',
|
|
335
|
+
scope: tokenData.scope,
|
|
336
|
+
expiresAt
|
|
337
|
+
});
|
|
338
|
+
return {
|
|
339
|
+
success: true
|
|
340
|
+
};
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
343
|
+
return {
|
|
344
|
+
success: false,
|
|
345
|
+
error: message
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
154
350
|
* Generate a random code verifier for PKCE
|
|
155
351
|
*/ generateCodeVerifier() {
|
|
156
352
|
const array = new Uint8Array(32);
|