Skip to content

Commit 15819d8

Browse files
committed
Revert "Feat/mcp for dashboard users (#2)"
This reverts commit ea08ba4.
1 parent ea08ba4 commit 15819d8

23 files changed

+2801
-524
lines changed

docs/api/TOOLS.md

Lines changed: 650 additions & 0 deletions
Large diffs are not rendered by default.

src/admin/admin.controller.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Controller, Post, Body, UseGuards, HttpCode, Logger } from '@nestjs/common';
2+
import { ApiTags, ApiOperation, ApiResponse, ApiSecurity } from '@nestjs/swagger';
3+
4+
import { AdminAuthGuard } from './guards/admin-auth.guard';
5+
import { UpdateConfigDto, UpdateConfigResponseDto } from './dto/update-config.dto';
6+
import { ApiClientService } from '../mcp/services/api-client.service';
7+
8+
@ApiTags('Admin')
9+
@Controller('admin')
10+
@UseGuards(AdminAuthGuard)
11+
@ApiSecurity('x-admin-api-key')
12+
export class AdminController {
13+
private readonly logger = new Logger(AdminController.name);
14+
15+
constructor(private readonly apiClientService: ApiClientService) {}
16+
17+
@Post('config')
18+
@HttpCode(200)
19+
@ApiOperation({
20+
summary: 'Update runtime configuration',
21+
description:
22+
'Update IOT_API_BASE_URL and IOT_API_KEY without restarting the service. ' +
23+
'Requires x-admin-api-key header for authentication.',
24+
})
25+
@ApiResponse({
26+
status: 200,
27+
description: 'Configuration updated successfully',
28+
type: UpdateConfigResponseDto,
29+
})
30+
@ApiResponse({
31+
status: 400,
32+
description: 'Invalid request - at least one field must be provided',
33+
})
34+
@ApiResponse({
35+
status: 401,
36+
description: 'Unauthorized - missing or invalid admin API key',
37+
})
38+
updateConfig(@Body() dto: UpdateConfigDto): UpdateConfigResponseDto {
39+
const { iotApiBaseUrl, iotApiKey } = dto;
40+
41+
// Validate that at least one field is provided
42+
if (!iotApiBaseUrl && !iotApiKey) {
43+
this.logger.warn('Config update attempted with no fields');
44+
return {
45+
success: false,
46+
message: 'At least one configuration field must be provided',
47+
};
48+
}
49+
50+
// Update configuration
51+
const updatedFields: string[] = [];
52+
53+
if (iotApiBaseUrl) {
54+
this.apiClientService.updateBaseUrl(iotApiBaseUrl);
55+
updatedFields.push('iotApiBaseUrl');
56+
this.logger.log(`Updated IOT_API_BASE_URL to: ${iotApiBaseUrl}`);
57+
}
58+
59+
if (iotApiKey) {
60+
this.apiClientService.updateApiKey(iotApiKey);
61+
updatedFields.push('iotApiKey');
62+
this.logger.log('Updated IOT_API_KEY (value hidden for security)');
63+
}
64+
65+
// Build response
66+
const response: UpdateConfigResponseDto = {
67+
success: true,
68+
message: `Configuration updated successfully: ${updatedFields.join(', ')}`,
69+
updatedConfig: {
70+
...(iotApiBaseUrl && { iotApiBaseUrl }),
71+
...(iotApiKey && { iotApiKeyUpdated: true }),
72+
},
73+
};
74+
75+
return response;
76+
}
77+
}

src/admin/admin.module.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
3+
4+
import { AdminController } from './admin.controller';
5+
import { AdminAuthGuard } from './guards/admin-auth.guard';
6+
import { McpModule } from '../mcp/mcp.module';
7+
8+
@Module({
9+
imports: [ConfigModule, McpModule],
10+
controllers: [AdminController],
11+
providers: [AdminAuthGuard],
12+
})
13+
export class AdminModule {}

src/admin/dto/update-config.dto.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { IsString, IsOptional } from 'class-validator';
2+
import { ApiProperty } from '@nestjs/swagger';
3+
4+
export class UpdateConfigDto {
5+
@ApiProperty({
6+
description: 'IoT API base URL (e.g., https://api.iot-cloud.com)',
7+
required: false,
8+
example: 'https://api.iot-cloud.com',
9+
})
10+
@IsString()
11+
@IsOptional()
12+
iotApiBaseUrl?: string;
13+
14+
@ApiProperty({
15+
description: 'IoT API key for authentication',
16+
required: false,
17+
example: 'sk_live_1234567890abcdef',
18+
})
19+
@IsString()
20+
@IsOptional()
21+
iotApiKey?: string;
22+
}
23+
24+
export class UpdateConfigResponseDto {
25+
@ApiProperty({
26+
description: 'Operation success status',
27+
example: true,
28+
})
29+
success: boolean;
30+
31+
@ApiProperty({
32+
description: 'Success or error message',
33+
example: 'Configuration updated successfully',
34+
})
35+
message: string;
36+
37+
@ApiProperty({
38+
description: 'Updated configuration values (without sensitive data)',
39+
example: {
40+
iotApiBaseUrl: 'https://api.iot-cloud.com',
41+
iotApiKeyUpdated: true,
42+
},
43+
})
44+
updatedConfig?: {
45+
iotApiBaseUrl?: string;
46+
iotApiKeyUpdated?: boolean;
47+
};
48+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
Injectable,
3+
CanActivate,
4+
ExecutionContext,
5+
UnauthorizedException,
6+
Logger,
7+
} from '@nestjs/common';
8+
import { ConfigService } from '@nestjs/config';
9+
import { Request } from 'express';
10+
11+
@Injectable()
12+
export class AdminAuthGuard implements CanActivate {
13+
private readonly logger = new Logger(AdminAuthGuard.name);
14+
15+
constructor(private configService: ConfigService) {}
16+
17+
canActivate(context: ExecutionContext): boolean {
18+
const request = context.switchToHttp().getRequest<Request>();
19+
const adminKey = request.headers['x-admin-api-key'];
20+
const expectedKey = this.configService.get<string>('ADMIN_API_KEY');
21+
22+
if (!expectedKey) {
23+
this.logger.error('ADMIN_API_KEY is not configured in environment');
24+
throw new UnauthorizedException('Admin API is not configured');
25+
}
26+
27+
if (!adminKey) {
28+
this.logger.warn('Admin request without x-admin-api-key header');
29+
throw new UnauthorizedException('Missing admin API key');
30+
}
31+
32+
if (adminKey !== expectedKey) {
33+
this.logger.warn('Admin request with invalid API key');
34+
throw new UnauthorizedException('Invalid admin API key');
35+
}
36+
37+
this.logger.debug('Admin authentication successful');
38+
return true;
39+
}
40+
}

src/api/api.module.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@nestjs/common';
2+
import { HttpModule } from '@nestjs/axios';
3+
import { AuthModule } from '../auth/auth.module';
4+
import { McpModule } from '../mcp/mcp.module';
5+
import { ApiClientService } from '../mcp/services/api-client.service';
6+
import { McpController } from './controllers/mcp.controller';
7+
8+
@Module({
9+
imports: [HttpModule, AuthModule, McpModule],
10+
controllers: [McpController],
11+
providers: [ApiClientService],
12+
})
13+
export class ApiModule {}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Controller, All, Req, Res } from '@nestjs/common';
2+
import { ApiTags, ApiOperation, ApiResponse, ApiExcludeEndpoint } from '@nestjs/swagger';
3+
import { Response, Request } from 'express';
4+
import { randomUUID } from 'node:crypto';
5+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6+
import { McpService } from '../../mcp/services/mcp.service';
7+
8+
@ApiTags('MCP Protocol')
9+
@Controller('mcp')
10+
export class McpController {
11+
// Store transports by session ID (stateful mode)
12+
private readonly transports = new Map<string, StreamableHTTPServerTransport>();
13+
14+
constructor(private mcpService: McpService) {}
15+
16+
/**
17+
* Main MCP endpoint - handles all HTTP methods
18+
* GET: Establishes SSE stream for server-to-client messages
19+
* POST: Receives client-to-server JSON-RPC messages
20+
* DELETE: Terminates session
21+
*
22+
* NO AUTHENTICATION - Clients connect here, then use login tool
23+
*/
24+
@All()
25+
@ApiOperation({
26+
summary: 'MCP Streamable HTTP endpoint',
27+
description:
28+
'Main MCP endpoint using official SDK transport. No authentication required - use login tool to authenticate. ' +
29+
'Supports GET (SSE stream), POST (JSON-RPC messages), DELETE (session termination).',
30+
})
31+
@ApiResponse({
32+
status: 200,
33+
description: 'Request handled successfully',
34+
})
35+
@ApiExcludeEndpoint() // Hide from Swagger since it's a special protocol endpoint
36+
async handleMcpRequest(@Req() req: Request, @Res() res: Response): Promise<void> {
37+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
38+
39+
console.log(`[MCP] ${req.method} request`, {
40+
sessionId,
41+
hasBody: !!req.body,
42+
url: req.url,
43+
});
44+
45+
try {
46+
let transport: StreamableHTTPServerTransport;
47+
48+
if (sessionId && this.transports.has(sessionId)) {
49+
// Reuse existing transport for this session
50+
transport = this.transports.get(sessionId)!;
51+
console.log(`[MCP] Reusing transport for session: ${sessionId}`);
52+
} else {
53+
// Create new transport (stateful mode with session management)
54+
transport = new StreamableHTTPServerTransport({
55+
sessionIdGenerator: () => randomUUID(),
56+
onsessioninitialized: (newSessionId) => {
57+
// Store transport when session is initialized
58+
console.log(`[MCP] Session initialized: ${newSessionId}`);
59+
this.transports.set(newSessionId, transport);
60+
},
61+
});
62+
63+
// Set up cleanup handler
64+
transport.onclose = () => {
65+
const sid = transport.sessionId;
66+
if (sid && this.transports.has(sid)) {
67+
console.log(`[MCP] Transport closed for session: ${sid}`);
68+
this.transports.delete(sid);
69+
}
70+
};
71+
72+
// CRITICAL FIX: Create NEW server instance per transport
73+
// Each transport needs its own server connection
74+
console.log('[MCP] Creating new server instance for transport');
75+
const server = await this.mcpService.createServer();
76+
await server.connect(transport);
77+
}
78+
79+
// Handle the request using the SDK transport
80+
// The transport handles SSE streaming, JSON-RPC parsing, session validation, etc.
81+
await transport.handleRequest(req, res, req.body);
82+
} catch (error) {
83+
console.error('[MCP] Error handling request:', error);
84+
if (!res.headersSent) {
85+
res.status(500).json({
86+
jsonrpc: '2.0',
87+
error: {
88+
code: -32603,
89+
message: 'Internal server error',
90+
},
91+
id: null,
92+
});
93+
}
94+
}
95+
}
96+
}

src/app.module.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { Module } from '@nestjs/common';
22
import { ConfigModule } from '@nestjs/config';
33
import { ThrottlerModule } from '@nestjs/throttler';
44
import { HttpModule } from '@nestjs/axios';
5+
6+
import { AuthModule } from './auth/auth.module';
7+
import { ApiModule } from './api/api.module';
8+
import { AdminModule } from './admin/admin.module';
59
import { HealthController } from './health.controller';
6-
import { McpV2Module } from '@/mcp_v2/mcp-v2.module';
710

811
@Module({
912
imports: [
@@ -26,7 +29,11 @@ import { McpV2Module } from '@/mcp_v2/mcp-v2.module';
2629
timeout: 30000,
2730
maxRedirects: 5,
2831
}),
29-
McpV2Module
32+
33+
// Feature modules
34+
AuthModule,
35+
ApiModule,
36+
AdminModule,
3037
],
3138
controllers: [HealthController],
3239
})

0 commit comments

Comments
 (0)