@dxheroes/local-mcp-backend 0.4.0 → 0.5.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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/AGENTS.md +4 -0
  3. package/CHANGELOG.md +32 -0
  4. package/dist/app.module.js +5 -0
  5. package/dist/app.module.js.map +1 -1
  6. package/dist/modules/profiles/profiles.service.js +15 -0
  7. package/dist/modules/profiles/profiles.service.js.map +1 -1
  8. package/dist/modules/proxy/proxy.controller.js +258 -9
  9. package/dist/modules/proxy/proxy.controller.js.map +1 -1
  10. package/dist/modules/proxy/proxy.module.js +4 -1
  11. package/dist/modules/proxy/proxy.module.js.map +1 -1
  12. package/dist/modules/settings/settings.constants.js +18 -0
  13. package/dist/modules/settings/settings.constants.js.map +1 -0
  14. package/dist/modules/settings/settings.controller.js +89 -0
  15. package/dist/modules/settings/settings.controller.js.map +1 -0
  16. package/dist/modules/settings/settings.module.js +30 -0
  17. package/dist/modules/settings/settings.module.js.map +1 -0
  18. package/dist/modules/settings/settings.service.js +110 -0
  19. package/dist/modules/settings/settings.service.js.map +1 -0
  20. package/package.json +6 -5
  21. package/src/AGENTS.md +23 -0
  22. package/src/app.module.ts +6 -0
  23. package/src/common/AGENTS.md +19 -0
  24. package/src/config/AGENTS.md +17 -0
  25. package/src/modules/AGENTS.md +31 -0
  26. package/src/modules/database/AGENTS.md +30 -0
  27. package/src/modules/debug/AGENTS.md +30 -0
  28. package/src/modules/health/AGENTS.md +22 -0
  29. package/src/modules/mcp/AGENTS.md +38 -0
  30. package/src/modules/oauth/AGENTS.md +32 -0
  31. package/src/modules/profiles/AGENTS.md +33 -0
  32. package/src/modules/profiles/profiles.service.ts +19 -0
  33. package/src/modules/proxy/AGENTS.md +34 -0
  34. package/src/modules/proxy/proxy.controller.ts +249 -7
  35. package/src/modules/proxy/proxy.module.ts +3 -1
  36. package/src/modules/settings/AGENTS.md +31 -0
  37. package/src/modules/settings/settings.constants.ts +20 -0
  38. package/src/modules/settings/settings.controller.ts +47 -0
  39. package/src/modules/settings/settings.module.ts +16 -0
  40. package/src/modules/settings/settings.service.ts +99 -0
@@ -13,6 +13,7 @@ import {
13
13
  } from '@nestjs/common';
14
14
  import { PrismaService } from '../database/prisma.service.js';
15
15
  import { ProxyService } from '../proxy/proxy.service.js';
16
+ import { RESERVED_PROFILE_NAMES } from '../settings/settings.constants.js';
16
17
 
17
18
  interface CreateProfileDto {
18
19
  name: string;
@@ -51,6 +52,16 @@ export class ProfilesService {
51
52
  private readonly proxyService: ProxyService
52
53
  ) {}
53
54
 
55
+ /**
56
+ * Validate profile name against reserved names
57
+ */
58
+ private validateProfileName(name: string): void {
59
+ const lowerName = name.toLowerCase();
60
+ if (RESERVED_PROFILE_NAMES.some((reserved) => reserved.toLowerCase() === lowerName)) {
61
+ throw new ConflictException(`Profile name "${name}" is reserved for system use`);
62
+ }
63
+ }
64
+
54
65
  /**
55
66
  * Get all profiles
56
67
  */
@@ -128,6 +139,9 @@ export class ProfilesService {
128
139
  * Create a new profile
129
140
  */
130
141
  async create(dto: CreateProfileDto) {
142
+ // Validate against reserved names
143
+ this.validateProfileName(dto.name);
144
+
131
145
  // Check for unique name
132
146
  const existing = await this.prisma.profile.findUnique({
133
147
  where: { name: dto.name },
@@ -155,6 +169,11 @@ export class ProfilesService {
155
169
  throw new NotFoundException(`Profile ${id} not found`);
156
170
  }
157
171
 
172
+ // Validate new name against reserved names
173
+ if (dto.name) {
174
+ this.validateProfileName(dto.name);
175
+ }
176
+
158
177
  // Check for unique name if changing
159
178
  if (dto.name && dto.name !== profile.name) {
160
179
  const existing = await this.prisma.profile.findUnique({
@@ -0,0 +1,34 @@
1
+ # Proxy Module
2
+
3
+ ## Description
4
+
5
+ MCP proxy module that exposes profile endpoints for AI clients. Handles MCP protocol communication (HTTP/SSE) and routes requests to underlying servers.
6
+
7
+ ## Contents
8
+
9
+ - `proxy.module.ts` - Module definition
10
+ - `proxy.controller.ts` - MCP protocol endpoints (HTTP, SSE)
11
+ - `proxy.service.ts` - Request routing and tool aggregation
12
+
13
+ ## Key Endpoints
14
+
15
+ - `POST /api/mcp/:profileSlug` - HTTP MCP endpoint (Streamable HTTP)
16
+ - `GET /api/mcp/:profileSlug/sse` - SSE MCP endpoint (legacy)
17
+ - `POST /api/mcp/:profileSlug/message` - SSE message endpoint (legacy)
18
+
19
+ ## Key Concepts
20
+
21
+ - **Streamable HTTP**: Modern MCP transport (single POST endpoint)
22
+ - **SSE Transport**: Legacy Server-Sent Events transport
23
+ - **Tool Aggregation**: Combines tools from all profile MCP servers
24
+ - **Request Routing**: Routes tool calls to correct MCP server
25
+ - **Debug Logging**: All traffic is logged for debugging
26
+
27
+ ## Proxy Flow
28
+
29
+ 1. AI client connects to profile endpoint
30
+ 2. Proxy aggregates tool lists from all servers
31
+ 3. Client calls a tool
32
+ 4. Proxy routes to correct MCP server
33
+ 5. Response returned to client
34
+ 6. All traffic logged to debug logs
@@ -1,16 +1,235 @@
1
1
  /**
2
2
  * Proxy Controller
3
3
  *
4
- * MCP proxy endpoints for profiles.
4
+ * MCP proxy endpoints for profiles and gateway.
5
+ * Supports MCP Streamable HTTP transport (2025-11-25 spec) with SSE notifications.
6
+ *
7
+ * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http
5
8
  */
6
9
 
7
- import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
10
+ import {
11
+ Body,
12
+ Controller,
13
+ Get,
14
+ HttpCode,
15
+ HttpStatus,
16
+ NotFoundException,
17
+ Param,
18
+ Post,
19
+ Req,
20
+ Res,
21
+ } from '@nestjs/common';
22
+ import { EventEmitter2 } from '@nestjs/event-emitter';
23
+ import type { Request, Response } from 'express';
24
+ import { fromEvent, map } from 'rxjs';
25
+ import { GATEWAY_PROFILE_CHANGED, SettingsService } from '../settings/settings.service.js';
8
26
  import type { McpRequest, McpResponse } from './proxy.service.js';
9
27
  import { ProxyService } from './proxy.service.js';
10
28
 
11
29
  @Controller('mcp')
12
30
  export class ProxyController {
13
- constructor(private readonly proxyService: ProxyService) {}
31
+ constructor(
32
+ private readonly proxyService: ProxyService,
33
+ private readonly settingsService: SettingsService,
34
+ private readonly eventEmitter: EventEmitter2
35
+ ) {}
36
+
37
+ // =========================================
38
+ // Gateway Endpoints (must come BEFORE :profileName routes)
39
+ // =========================================
40
+
41
+ /**
42
+ * SSE endpoint for gateway notifications (dedicated URL)
43
+ * Sends notifications/tools/list_changed when the active profile changes.
44
+ */
45
+ @Get('gateway/sse')
46
+ streamGatewaySse(@Req() req: Request, @Res() res: Response) {
47
+ return this.streamGatewayEvents(req, res);
48
+ }
49
+
50
+ /**
51
+ * POST handler for gateway SSE - handles JSON-RPC requests via SSE transport
52
+ */
53
+ @Post('gateway/sse')
54
+ @HttpCode(HttpStatus.OK)
55
+ async handleGatewaySseRequest(@Body() request: McpRequest): Promise<McpResponse> {
56
+ return this.handleGatewayRequest(request);
57
+ }
58
+
59
+ /**
60
+ * Get gateway info - proxies to default profile info
61
+ */
62
+ @Get('gateway/info')
63
+ async getGatewayInfo() {
64
+ const profileName = await this.settingsService.getDefaultGatewayProfile();
65
+
66
+ try {
67
+ const info = await this.proxyService.getProfileInfo(profileName);
68
+ return {
69
+ ...info,
70
+ gateway: {
71
+ activeProfile: profileName,
72
+ },
73
+ };
74
+ } catch (error) {
75
+ if (error instanceof NotFoundException) {
76
+ throw new NotFoundException(
77
+ `Default gateway profile "${profileName}" not found. Please select a valid profile in settings.`
78
+ );
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * GET handler for gateway endpoint
86
+ *
87
+ * Streamable HTTP content negotiation:
88
+ * - Accept: text/event-stream -> Returns SSE stream for notifications
89
+ * - Otherwise -> Returns JSON usage instructions
90
+ *
91
+ * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http
92
+ */
93
+ @Get('gateway')
94
+ async getGatewayEndpoint(@Req() req: Request, @Res() res: Response) {
95
+ // Streamable HTTP: check Accept header for content negotiation
96
+ const acceptHeader = req.headers.accept || '';
97
+ if (acceptHeader.includes('text/event-stream')) {
98
+ return this.streamGatewayEvents(req, res);
99
+ }
100
+
101
+ // Return JSON usage info
102
+ const profileName = await this.settingsService.getDefaultGatewayProfile();
103
+
104
+ try {
105
+ const info = await this.proxyService.getProfileInfo(profileName);
106
+ return res.json({
107
+ message: 'This is the MCP Gateway endpoint. Use POST for JSON-RPC requests.',
108
+ usage: {
109
+ method: 'POST',
110
+ contentType: 'application/json',
111
+ body: {
112
+ jsonrpc: '2.0',
113
+ method: 'tools/list',
114
+ id: 1,
115
+ },
116
+ },
117
+ endpoints: {
118
+ sse: '/api/mcp/gateway/sse',
119
+ http: '/api/mcp/gateway',
120
+ },
121
+ gateway: {
122
+ activeProfile: profileName,
123
+ settingsEndpoint: '/api/settings/default-gateway-profile',
124
+ },
125
+ profile: {
126
+ name: profileName,
127
+ toolCount: info.tools.length,
128
+ serverCount: info.serverStatus.total,
129
+ connectedServers: info.serverStatus.connected,
130
+ },
131
+ infoEndpoint: '/api/mcp/gateway/info',
132
+ });
133
+ } catch (error) {
134
+ if (error instanceof NotFoundException) {
135
+ throw new NotFoundException(
136
+ `Default gateway profile "${profileName}" not found. Please select a valid profile in settings.`
137
+ );
138
+ }
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Stream SSE events for gateway notifications.
145
+ * Sends notifications/tools/list_changed when the active profile changes.
146
+ *
147
+ * Follows MCP Streamable HTTP transport (2025-11-25):
148
+ * - No endpoint event needed (unified endpoint)
149
+ * - Simple data: format without event: prefix
150
+ *
151
+ * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http
152
+ */
153
+ private streamGatewayEvents(req: Request, res: Response) {
154
+ // Set SSE headers
155
+ res.setHeader('Content-Type', 'text/event-stream');
156
+ res.setHeader('Cache-Control', 'no-cache');
157
+ res.setHeader('Connection', 'keep-alive');
158
+ res.setHeader('X-Accel-Buffering', 'no'); // For nginx
159
+
160
+ // Streamable HTTP - no endpoint event needed
161
+ // Just send a comment to establish connection
162
+ res.write(': connected\n\n');
163
+
164
+ // Subscribe to profile change events
165
+ const subscription = fromEvent(this.eventEmitter, GATEWAY_PROFILE_CHANGED)
166
+ .pipe(
167
+ map(() => ({
168
+ jsonrpc: '2.0',
169
+ method: 'notifications/tools/list_changed',
170
+ }))
171
+ )
172
+ .subscribe((notification) => {
173
+ // Streamable HTTP - simple data format, no event prefix
174
+ res.write(`data: ${JSON.stringify(notification)}\n\n`);
175
+ });
176
+
177
+ // Cleanup on client disconnect
178
+ req.on('close', () => {
179
+ subscription.unsubscribe();
180
+ });
181
+ }
182
+
183
+ /**
184
+ * MCP JSON-RPC endpoint for the gateway - proxies to default profile
185
+ */
186
+ @Post('gateway')
187
+ @HttpCode(HttpStatus.OK)
188
+ async handleGatewayRequest(@Body() request: McpRequest): Promise<McpResponse> {
189
+ const profileName = await this.settingsService.getDefaultGatewayProfile();
190
+
191
+ try {
192
+ return await this.proxyService.handleRequest(profileName, request);
193
+ } catch (error) {
194
+ if (error instanceof NotFoundException) {
195
+ throw new NotFoundException(
196
+ `Default gateway profile "${profileName}" not found. Please select a valid profile in settings.`
197
+ );
198
+ }
199
+ throw error;
200
+ }
201
+ }
202
+
203
+ // =========================================
204
+ // Profile-specific Endpoints (existing)
205
+ // =========================================
206
+
207
+ /**
208
+ * SSE endpoint for profile notifications (dedicated URL)
209
+ * Sends notifications/tools/list_changed when the active gateway profile changes.
210
+ */
211
+ @Get(':profileName/sse')
212
+ async streamProfileSse(
213
+ @Param('profileName') profileName: string,
214
+ @Req() req: Request,
215
+ @Res() res: Response
216
+ ) {
217
+ // Validate profile exists
218
+ await this.proxyService.getProfileInfo(profileName);
219
+ return this.streamGatewayEvents(req, res);
220
+ }
221
+
222
+ /**
223
+ * POST handler for profile SSE - handles JSON-RPC requests via SSE transport
224
+ */
225
+ @Post(':profileName/sse')
226
+ @HttpCode(HttpStatus.OK)
227
+ async handleProfileSseRequest(
228
+ @Param('profileName') profileName: string,
229
+ @Body() request: McpRequest
230
+ ): Promise<McpResponse> {
231
+ return this.handleMcpRequest(profileName, request);
232
+ }
14
233
 
15
234
  /**
16
235
  * Get profile info with aggregated tools and server status
@@ -21,12 +240,31 @@ export class ProxyController {
21
240
  }
22
241
 
23
242
  /**
24
- * GET handler for MCP endpoint - returns usage instructions instead of 404
243
+ * GET handler for MCP endpoint
244
+ *
245
+ * Streamable HTTP content negotiation:
246
+ * - Accept: text/event-stream -> Returns SSE stream for notifications
247
+ * - Otherwise -> Returns JSON usage instructions
248
+ *
249
+ * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http
25
250
  */
26
251
  @Get(':profileName')
27
- async getMcpEndpoint(@Param('profileName') profileName: string) {
252
+ async getMcpEndpoint(
253
+ @Param('profileName') profileName: string,
254
+ @Req() req: Request,
255
+ @Res() res: Response
256
+ ) {
257
+ // Validate profile exists first
28
258
  const info = await this.proxyService.getProfileInfo(profileName);
29
- return {
259
+
260
+ // Streamable HTTP: check Accept header for content negotiation
261
+ const acceptHeader = req.headers.accept || '';
262
+ if (acceptHeader.includes('text/event-stream')) {
263
+ return this.streamGatewayEvents(req, res);
264
+ }
265
+
266
+ // Return JSON usage info
267
+ return res.json({
30
268
  message: 'This is an MCP (Model Context Protocol) endpoint. Use POST for JSON-RPC requests.',
31
269
  usage: {
32
270
  method: 'POST',
@@ -37,6 +275,10 @@ export class ProxyController {
37
275
  id: 1,
38
276
  },
39
277
  },
278
+ endpoints: {
279
+ sse: `/api/mcp/${profileName}/sse`,
280
+ http: `/api/mcp/${profileName}`,
281
+ },
40
282
  profile: {
41
283
  name: profileName,
42
284
  toolCount: info.tools.length,
@@ -44,7 +286,7 @@ export class ProxyController {
44
286
  connectedServers: info.serverStatus.connected,
45
287
  },
46
288
  infoEndpoint: `/api/mcp/${profileName}/info`,
47
- };
289
+ });
48
290
  }
49
291
 
50
292
  /**
@@ -2,16 +2,18 @@
2
2
  * Proxy Module
3
3
  *
4
4
  * Handles MCP proxy endpoints for profiles.
5
+ * Supports SSE notifications for MCP Streamable HTTP transport.
5
6
  */
6
7
 
7
8
  import { Module } from '@nestjs/common';
8
9
  import { DebugModule } from '../debug/debug.module.js';
9
10
  import { McpModule } from '../mcp/mcp.module.js';
11
+ import { SettingsModule } from '../settings/settings.module.js';
10
12
  import { ProxyController } from './proxy.controller.js';
11
13
  import { ProxyService } from './proxy.service.js';
12
14
 
13
15
  @Module({
14
- imports: [McpModule, DebugModule],
16
+ imports: [McpModule, DebugModule, SettingsModule],
15
17
  controllers: [ProxyController],
16
18
  providers: [ProxyService],
17
19
  exports: [ProxyService],
@@ -0,0 +1,31 @@
1
+ # Settings Module
2
+
3
+ ## Description
4
+
5
+ Application settings module. Manages gateway-wide configuration stored in the database.
6
+
7
+ ## Contents
8
+
9
+ - `settings.module.ts` - Module definition
10
+ - `settings.controller.ts` - REST API for settings
11
+ - `settings.service.ts` - Settings storage and retrieval
12
+ - `settings.constants.ts` - Default settings values
13
+
14
+ ## Key Endpoints
15
+
16
+ - `GET /api/settings` - Get all settings
17
+ - `PUT /api/settings` - Update settings
18
+ - `GET /api/settings/:key` - Get single setting
19
+
20
+ ## Key Settings
21
+
22
+ - `mcpTransport` - Default MCP transport type (http/sse)
23
+ - `debugLogRetention` - Debug log retention period
24
+ - Other gateway-wide configuration
25
+
26
+ ## Key Concepts
27
+
28
+ - **Key-Value Storage**: Settings stored as key-value pairs
29
+ - **Defaults**: Default values defined in constants
30
+ - **Persistence**: Settings persist across restarts
31
+ - **Type Safety**: Settings validated against expected types
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Settings Constants
3
+ *
4
+ * Defines setting keys, defaults, and reserved profile names.
5
+ */
6
+
7
+ // Setting keys
8
+ export const SETTING_KEYS = {
9
+ DEFAULT_GATEWAY_PROFILE: 'default_gateway_profile',
10
+ } as const;
11
+
12
+ // Default values for settings
13
+ export const SETTING_DEFAULTS: Record<string, string> = {
14
+ [SETTING_KEYS.DEFAULT_GATEWAY_PROFILE]: 'default',
15
+ };
16
+
17
+ // Reserved profile names that cannot be created by users
18
+ export const RESERVED_PROFILE_NAMES = ['gateway'] as const;
19
+
20
+ export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Settings Controller
3
+ *
4
+ * REST API endpoints for gateway settings management.
5
+ */
6
+
7
+ import { Body, Controller, Get, Put } from '@nestjs/common';
8
+ import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
9
+ import { SettingsService } from './settings.service.js';
10
+
11
+ export class UpdateDefaultGatewayProfileDto {
12
+ @IsString()
13
+ @IsNotEmpty({ message: 'Profile name cannot be empty' })
14
+ @MaxLength(100, { message: 'Profile name must be at most 100 characters' })
15
+ profileName: string;
16
+ }
17
+
18
+ @Controller('settings')
19
+ export class SettingsController {
20
+ constructor(private readonly settingsService: SettingsService) {}
21
+
22
+ /**
23
+ * Get all gateway settings
24
+ */
25
+ @Get()
26
+ async getAllSettings() {
27
+ return this.settingsService.getAllSettings();
28
+ }
29
+
30
+ /**
31
+ * Get the default gateway profile setting
32
+ */
33
+ @Get('default-gateway-profile')
34
+ async getDefaultGatewayProfile() {
35
+ const profileName = await this.settingsService.getDefaultGatewayProfile();
36
+ return { profileName };
37
+ }
38
+
39
+ /**
40
+ * Set the default gateway profile
41
+ */
42
+ @Put('default-gateway-profile')
43
+ async setDefaultGatewayProfile(@Body() dto: UpdateDefaultGatewayProfileDto) {
44
+ await this.settingsService.setDefaultGatewayProfile(dto.profileName);
45
+ return { profileName: dto.profileName };
46
+ }
47
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Settings Module
3
+ *
4
+ * Handles gateway configuration settings.
5
+ */
6
+
7
+ import { Module } from '@nestjs/common';
8
+ import { SettingsController } from './settings.controller.js';
9
+ import { SettingsService } from './settings.service.js';
10
+
11
+ @Module({
12
+ controllers: [SettingsController],
13
+ providers: [SettingsService],
14
+ exports: [SettingsService],
15
+ })
16
+ export class SettingsModule {}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Settings Service
3
+ *
4
+ * Business logic for gateway settings management.
5
+ * Emits events when gateway settings change for SSE notifications.
6
+ */
7
+
8
+ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
9
+ import { EventEmitter2 } from '@nestjs/event-emitter';
10
+ import { PrismaService } from '../database/prisma.service.js';
11
+ import { SETTING_DEFAULTS, SETTING_KEYS, type SettingKey } from './settings.constants.js';
12
+
13
+ /** Event name for gateway profile changes */
14
+ export const GATEWAY_PROFILE_CHANGED = 'gateway.profile.changed';
15
+
16
+ @Injectable()
17
+ export class SettingsService {
18
+ constructor(
19
+ private readonly prisma: PrismaService,
20
+ private readonly eventEmitter: EventEmitter2
21
+ ) {}
22
+
23
+ /**
24
+ * Get a setting value by key, returns default if not set
25
+ */
26
+ async getSetting(key: SettingKey): Promise<string> {
27
+ const setting = await this.prisma.gatewaySetting.findUnique({
28
+ where: { key },
29
+ });
30
+
31
+ return setting?.value ?? SETTING_DEFAULTS[key];
32
+ }
33
+
34
+ /**
35
+ * Set a setting value
36
+ */
37
+ async setSetting(key: SettingKey, value: string): Promise<{ key: string; value: string }> {
38
+ const setting = await this.prisma.gatewaySetting.upsert({
39
+ where: { key },
40
+ update: { value },
41
+ create: { key, value },
42
+ });
43
+
44
+ return { key: setting.key, value: setting.value };
45
+ }
46
+
47
+ /**
48
+ * Get the default gateway profile name
49
+ */
50
+ async getDefaultGatewayProfile(): Promise<string> {
51
+ return this.getSetting(SETTING_KEYS.DEFAULT_GATEWAY_PROFILE);
52
+ }
53
+
54
+ /**
55
+ * Set the default gateway profile
56
+ * Validates that the profile exists before setting.
57
+ * Emits GATEWAY_PROFILE_CHANGED event for SSE subscribers.
58
+ */
59
+ async setDefaultGatewayProfile(profileName: string): Promise<{ key: string; value: string }> {
60
+ // Input sanitization
61
+ const trimmedName = profileName?.trim();
62
+ if (!trimmedName) {
63
+ throw new BadRequestException('Profile name cannot be empty');
64
+ }
65
+
66
+ // Validate that profile exists
67
+ const profile = await this.prisma.profile.findUnique({
68
+ where: { name: trimmedName },
69
+ });
70
+
71
+ if (!profile) {
72
+ throw new NotFoundException(`Profile "${trimmedName}" not found`);
73
+ }
74
+
75
+ const result = await this.setSetting(SETTING_KEYS.DEFAULT_GATEWAY_PROFILE, trimmedName);
76
+
77
+ // Emit event for SSE subscribers (MCP Streamable HTTP notifications)
78
+ this.eventEmitter.emit(GATEWAY_PROFILE_CHANGED, { profileName: trimmedName });
79
+
80
+ return result;
81
+ }
82
+
83
+ /**
84
+ * Get all settings with their current values
85
+ */
86
+ async getAllSettings(): Promise<Record<string, string>> {
87
+ const settings = await this.prisma.gatewaySetting.findMany();
88
+
89
+ // Start with defaults
90
+ const result: Record<string, string> = { ...SETTING_DEFAULTS };
91
+
92
+ // Override with stored values
93
+ for (const setting of settings) {
94
+ result[setting.key] = setting.value;
95
+ }
96
+
97
+ return result;
98
+ }
99
+ }