@dxheroes/local-mcp-backend 0.3.1
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/.swcrc +22 -0
- package/.turbo/turbo-build.log +9 -0
- package/AGENTS.md +360 -0
- package/CHANGELOG.md +60 -0
- package/Dockerfile +71 -0
- package/LICENSE +94 -0
- package/dist/app.module.js +72 -0
- package/dist/app.module.js.map +1 -0
- package/dist/common/decorators/request-id.decorator.js +12 -0
- package/dist/common/decorators/request-id.decorator.js.map +1 -0
- package/dist/common/filters/all-exceptions.filter.js +61 -0
- package/dist/common/filters/all-exceptions.filter.js.map +1 -0
- package/dist/common/interceptors/logging.interceptor.js +46 -0
- package/dist/common/interceptors/logging.interceptor.js.map +1 -0
- package/dist/common/pipes/validation.pipe.js +43 -0
- package/dist/common/pipes/validation.pipe.js.map +1 -0
- package/dist/config/app.config.js +14 -0
- package/dist/config/app.config.js.map +1 -0
- package/dist/config/database.config.js +30 -0
- package/dist/config/database.config.js.map +1 -0
- package/dist/main.js +68 -0
- package/dist/main.js.map +1 -0
- package/dist/modules/database/database.module.js +27 -0
- package/dist/modules/database/database.module.js.map +1 -0
- package/dist/modules/database/prisma.service.js +122 -0
- package/dist/modules/database/prisma.service.js.map +1 -0
- package/dist/modules/debug/debug.controller.js +87 -0
- package/dist/modules/debug/debug.controller.js.map +1 -0
- package/dist/modules/debug/debug.module.js +30 -0
- package/dist/modules/debug/debug.module.js.map +1 -0
- package/dist/modules/debug/debug.service.js +126 -0
- package/dist/modules/debug/debug.service.js.map +1 -0
- package/dist/modules/health/health.controller.js +69 -0
- package/dist/modules/health/health.controller.js.map +1 -0
- package/dist/modules/health/health.module.js +23 -0
- package/dist/modules/health/health.module.js.map +1 -0
- package/dist/modules/mcp/dto/create-mcp-server.dto.js +74 -0
- package/dist/modules/mcp/dto/create-mcp-server.dto.js.map +1 -0
- package/dist/modules/mcp/dto/update-mcp-server.dto.js +74 -0
- package/dist/modules/mcp/dto/update-mcp-server.dto.js.map +1 -0
- package/dist/modules/mcp/mcp-discovery.service.js +176 -0
- package/dist/modules/mcp/mcp-discovery.service.js.map +1 -0
- package/dist/modules/mcp/mcp-registry.js +67 -0
- package/dist/modules/mcp/mcp-registry.js.map +1 -0
- package/dist/modules/mcp/mcp-seed.service.js +122 -0
- package/dist/modules/mcp/mcp-seed.service.js.map +1 -0
- package/dist/modules/mcp/mcp.controller.js +152 -0
- package/dist/modules/mcp/mcp.controller.js.map +1 -0
- package/dist/modules/mcp/mcp.module.js +70 -0
- package/dist/modules/mcp/mcp.module.js.map +1 -0
- package/dist/modules/mcp/mcp.service.js +401 -0
- package/dist/modules/mcp/mcp.service.js.map +1 -0
- package/dist/modules/oauth/oauth.controller.js +116 -0
- package/dist/modules/oauth/oauth.controller.js.map +1 -0
- package/dist/modules/oauth/oauth.module.js +31 -0
- package/dist/modules/oauth/oauth.module.js.map +1 -0
- package/dist/modules/oauth/oauth.service.js +183 -0
- package/dist/modules/oauth/oauth.service.js.map +1 -0
- package/dist/modules/profiles/profiles.controller.js +241 -0
- package/dist/modules/profiles/profiles.controller.js.map +1 -0
- package/dist/modules/profiles/profiles.module.js +34 -0
- package/dist/modules/profiles/profiles.module.js.map +1 -0
- package/dist/modules/profiles/profiles.service.js +390 -0
- package/dist/modules/profiles/profiles.service.js.map +1 -0
- package/dist/modules/proxy/proxy.controller.js +98 -0
- package/dist/modules/proxy/proxy.controller.js.map +1 -0
- package/dist/modules/proxy/proxy.module.js +36 -0
- package/dist/modules/proxy/proxy.module.js.map +1 -0
- package/dist/modules/proxy/proxy.service.js +439 -0
- package/dist/modules/proxy/proxy.service.js.map +1 -0
- package/docker-entrypoint.sh +10 -0
- package/nest-cli.json +10 -0
- package/package.json +51 -0
- package/src/app.module.ts +59 -0
- package/src/common/decorators/request-id.decorator.ts +16 -0
- package/src/common/filters/all-exceptions.filter.ts +77 -0
- package/src/common/interceptors/logging.interceptor.ts +45 -0
- package/src/common/pipes/validation.pipe.ts +31 -0
- package/src/config/app.config.ts +15 -0
- package/src/config/database.config.ts +34 -0
- package/src/main.ts +66 -0
- package/src/modules/database/database.module.ts +15 -0
- package/src/modules/database/prisma.service.ts +110 -0
- package/src/modules/debug/debug.controller.ts +53 -0
- package/src/modules/debug/debug.module.ts +16 -0
- package/src/modules/debug/debug.service.ts +143 -0
- package/src/modules/health/health.controller.ts +48 -0
- package/src/modules/health/health.module.ts +13 -0
- package/src/modules/mcp/dto/create-mcp-server.dto.ts +53 -0
- package/src/modules/mcp/dto/update-mcp-server.dto.ts +53 -0
- package/src/modules/mcp/mcp-discovery.service.ts +205 -0
- package/src/modules/mcp/mcp-registry.ts +73 -0
- package/src/modules/mcp/mcp-seed.service.ts +125 -0
- package/src/modules/mcp/mcp.controller.ts +98 -0
- package/src/modules/mcp/mcp.module.ts +48 -0
- package/src/modules/mcp/mcp.service.ts +427 -0
- package/src/modules/oauth/oauth.controller.ts +89 -0
- package/src/modules/oauth/oauth.module.ts +17 -0
- package/src/modules/oauth/oauth.service.ts +212 -0
- package/src/modules/profiles/profiles.controller.ts +177 -0
- package/src/modules/profiles/profiles.module.ts +18 -0
- package/src/modules/profiles/profiles.service.ts +421 -0
- package/src/modules/proxy/proxy.controller.ts +61 -0
- package/src/modules/proxy/proxy.module.ts +19 -0
- package/src/modules/proxy/proxy.service.ts +595 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Exception Filter
|
|
3
|
+
*
|
|
4
|
+
* Catches all exceptions and formats them consistently.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ArgumentsHost,
|
|
9
|
+
Catch,
|
|
10
|
+
ExceptionFilter,
|
|
11
|
+
HttpException,
|
|
12
|
+
HttpStatus,
|
|
13
|
+
Logger,
|
|
14
|
+
} from '@nestjs/common';
|
|
15
|
+
import { Request, Response } from 'express';
|
|
16
|
+
|
|
17
|
+
interface ErrorResponse {
|
|
18
|
+
statusCode: number;
|
|
19
|
+
message: string;
|
|
20
|
+
error: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
path: string;
|
|
23
|
+
requestId?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Catch()
|
|
27
|
+
export class AllExceptionsFilter implements ExceptionFilter {
|
|
28
|
+
private readonly logger = new Logger(AllExceptionsFilter.name);
|
|
29
|
+
|
|
30
|
+
catch(exception: unknown, host: ArgumentsHost): void {
|
|
31
|
+
const ctx = host.switchToHttp();
|
|
32
|
+
const response = ctx.getResponse<Response>();
|
|
33
|
+
const request = ctx.getRequest<Request>();
|
|
34
|
+
|
|
35
|
+
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
36
|
+
let message = 'Internal server error';
|
|
37
|
+
let error = 'Internal Server Error';
|
|
38
|
+
|
|
39
|
+
if (exception instanceof HttpException) {
|
|
40
|
+
status = exception.getStatus();
|
|
41
|
+
const exceptionResponse = exception.getResponse();
|
|
42
|
+
|
|
43
|
+
if (typeof exceptionResponse === 'string') {
|
|
44
|
+
message = exceptionResponse;
|
|
45
|
+
} else if (typeof exceptionResponse === 'object') {
|
|
46
|
+
const responseObj = exceptionResponse as Record<string, unknown>;
|
|
47
|
+
message = (responseObj.message as string) || message;
|
|
48
|
+
error = (responseObj.error as string) || exception.name;
|
|
49
|
+
}
|
|
50
|
+
} else if (exception instanceof Error) {
|
|
51
|
+
message = exception.message;
|
|
52
|
+
error = exception.name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Log error
|
|
56
|
+
this.logger.error(`${request.method} ${request.url} - ${status} - ${message}`, {
|
|
57
|
+
exception: exception instanceof Error ? exception.stack : String(exception),
|
|
58
|
+
requestId: request.headers['x-request-id'],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const errorResponse: ErrorResponse = {
|
|
62
|
+
statusCode: status,
|
|
63
|
+
message,
|
|
64
|
+
error,
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
path: request.url,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Add request ID if present
|
|
70
|
+
const requestId = request.headers['x-request-id'];
|
|
71
|
+
if (typeof requestId === 'string') {
|
|
72
|
+
errorResponse.requestId = requestId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
response.status(status).json(errorResponse);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging Interceptor
|
|
3
|
+
*
|
|
4
|
+
* Logs incoming requests and outgoing responses with timing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
|
|
9
|
+
import { Request, Response } from 'express';
|
|
10
|
+
import { Observable } from 'rxjs';
|
|
11
|
+
import { tap } from 'rxjs/operators';
|
|
12
|
+
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class LoggingInterceptor implements NestInterceptor {
|
|
15
|
+
private readonly logger = new Logger('HTTP');
|
|
16
|
+
|
|
17
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
|
18
|
+
const ctx = context.switchToHttp();
|
|
19
|
+
const request = ctx.getRequest<Request>();
|
|
20
|
+
const response = ctx.getResponse<Response>();
|
|
21
|
+
|
|
22
|
+
// Add request ID if not present
|
|
23
|
+
if (!request.headers['x-request-id']) {
|
|
24
|
+
request.headers['x-request-id'] = randomUUID();
|
|
25
|
+
}
|
|
26
|
+
const requestId = request.headers['x-request-id'];
|
|
27
|
+
response.setHeader('x-request-id', requestId);
|
|
28
|
+
|
|
29
|
+
const { method, url } = request;
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
|
|
32
|
+
return next.handle().pipe(
|
|
33
|
+
tap({
|
|
34
|
+
next: () => {
|
|
35
|
+
const duration = Date.now() - startTime;
|
|
36
|
+
this.logger.log(`${method} ${url} ${response.statusCode} - ${duration}ms`);
|
|
37
|
+
},
|
|
38
|
+
error: () => {
|
|
39
|
+
const duration = Date.now() - startTime;
|
|
40
|
+
this.logger.warn(`${method} ${url} ERROR - ${duration}ms`);
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod Validation Pipe
|
|
3
|
+
*
|
|
4
|
+
* Custom validation pipe that uses Zod schemas.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class ZodValidationPipe implements PipeTransform {
|
|
12
|
+
constructor(private schema: z.ZodSchema) {}
|
|
13
|
+
|
|
14
|
+
transform(value: unknown) {
|
|
15
|
+
const result = this.schema.safeParse(value);
|
|
16
|
+
|
|
17
|
+
if (!result.success) {
|
|
18
|
+
const errors = result.error.issues.map((issue) => ({
|
|
19
|
+
path: issue.path.join('.'),
|
|
20
|
+
message: issue.message,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
throw new BadRequestException({
|
|
24
|
+
message: 'Validation failed',
|
|
25
|
+
errors,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return result.data;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { registerAs } from '@nestjs/config';
|
|
6
|
+
|
|
7
|
+
export default registerAs('app', () => ({
|
|
8
|
+
nodeEnv: process.env.NODE_ENV || 'development',
|
|
9
|
+
port: Number.parseInt(process.env.PORT || '3001', 10),
|
|
10
|
+
corsOrigins: process.env.CORS_ORIGINS?.split(',') || [
|
|
11
|
+
'http://localhost:5173',
|
|
12
|
+
'http://localhost:3000',
|
|
13
|
+
],
|
|
14
|
+
logLevel: process.env.LOG_LEVEL || 'info',
|
|
15
|
+
}));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { registerAs } from '@nestjs/config';
|
|
8
|
+
|
|
9
|
+
export default registerAs('database', () => {
|
|
10
|
+
// Parse DATABASE_URL for path
|
|
11
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
12
|
+
|
|
13
|
+
// Default path if not set
|
|
14
|
+
const defaultPath = join(homedir(), '.local-mcp-gateway-data', 'local-mcp-gateway.db');
|
|
15
|
+
|
|
16
|
+
// Extract path from file: URL
|
|
17
|
+
let path = defaultPath;
|
|
18
|
+
if (databaseUrl?.startsWith('file:')) {
|
|
19
|
+
const filePath = databaseUrl.replace('file:', '');
|
|
20
|
+
if (!filePath.startsWith(':memory:')) {
|
|
21
|
+
// Handle relative paths (./dev.db)
|
|
22
|
+
if (filePath.startsWith('./')) {
|
|
23
|
+
path = join(process.cwd(), filePath.slice(2));
|
|
24
|
+
} else {
|
|
25
|
+
path = filePath;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
url: databaseUrl || `file:${defaultPath}`,
|
|
32
|
+
path,
|
|
33
|
+
};
|
|
34
|
+
});
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NestJS Application Bootstrap
|
|
3
|
+
*
|
|
4
|
+
* Entry point for the Local MCP Gateway backend.
|
|
5
|
+
* No authentication required - immediate access to all features.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Logger, ValidationPipe } from '@nestjs/common';
|
|
9
|
+
import { ConfigService } from '@nestjs/config';
|
|
10
|
+
import { NestFactory } from '@nestjs/core';
|
|
11
|
+
import compression from 'compression';
|
|
12
|
+
import helmet from 'helmet';
|
|
13
|
+
import { AppModule } from './app.module.js';
|
|
14
|
+
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter.js';
|
|
15
|
+
import { LoggingInterceptor } from './common/interceptors/logging.interceptor.js';
|
|
16
|
+
|
|
17
|
+
async function bootstrap() {
|
|
18
|
+
const logger = new Logger('Bootstrap');
|
|
19
|
+
const app = await NestFactory.create(AppModule, {
|
|
20
|
+
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const configService = app.get(ConfigService);
|
|
24
|
+
|
|
25
|
+
// Security
|
|
26
|
+
app.use(helmet());
|
|
27
|
+
app.use(compression());
|
|
28
|
+
|
|
29
|
+
// CORS
|
|
30
|
+
const corsOrigins = configService.get<string>('CORS_ORIGINS')?.split(',') || [
|
|
31
|
+
'http://localhost:5173',
|
|
32
|
+
'http://localhost:3000',
|
|
33
|
+
];
|
|
34
|
+
app.enableCors({
|
|
35
|
+
origin: corsOrigins,
|
|
36
|
+
credentials: true,
|
|
37
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Global prefix
|
|
41
|
+
app.setGlobalPrefix('api');
|
|
42
|
+
|
|
43
|
+
// Global pipes
|
|
44
|
+
app.useGlobalPipes(
|
|
45
|
+
new ValidationPipe({
|
|
46
|
+
whitelist: true,
|
|
47
|
+
forbidNonWhitelisted: true,
|
|
48
|
+
transform: true,
|
|
49
|
+
transformOptions: { enableImplicitConversion: true },
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Global filters
|
|
54
|
+
app.useGlobalFilters(new AllExceptionsFilter());
|
|
55
|
+
|
|
56
|
+
// Global interceptors
|
|
57
|
+
app.useGlobalInterceptors(new LoggingInterceptor());
|
|
58
|
+
|
|
59
|
+
const port = configService.get<number>('PORT') || 3001;
|
|
60
|
+
await app.listen(port);
|
|
61
|
+
|
|
62
|
+
logger.log(`Application is running on: http://localhost:${port}`);
|
|
63
|
+
logger.log(`API available at: http://localhost:${port}/api`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
bootstrap();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Module
|
|
3
|
+
*
|
|
4
|
+
* Global module providing Prisma client for database operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Global, Module } from '@nestjs/common';
|
|
8
|
+
import { PrismaService } from './prisma.service.js';
|
|
9
|
+
|
|
10
|
+
@Global()
|
|
11
|
+
@Module({
|
|
12
|
+
providers: [PrismaService],
|
|
13
|
+
exports: [PrismaService],
|
|
14
|
+
})
|
|
15
|
+
export class DatabaseModule {}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma Service
|
|
3
|
+
*
|
|
4
|
+
* NestJS service wrapping Prisma Client with lifecycle management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createPrismaAdapter } from '@dxheroes/local-mcp-database';
|
|
8
|
+
import { PrismaClient } from '@dxheroes/local-mcp-database/generated/prisma';
|
|
9
|
+
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
|
13
|
+
private readonly logger = new Logger(PrismaService.name);
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
super({
|
|
17
|
+
adapter: createPrismaAdapter(),
|
|
18
|
+
log:
|
|
19
|
+
process.env.NODE_ENV === 'development'
|
|
20
|
+
? [
|
|
21
|
+
{ emit: 'event', level: 'query' },
|
|
22
|
+
{ emit: 'stdout', level: 'info' },
|
|
23
|
+
{ emit: 'stdout', level: 'warn' },
|
|
24
|
+
{ emit: 'stdout', level: 'error' },
|
|
25
|
+
]
|
|
26
|
+
: [{ emit: 'stdout', level: 'error' }],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async onModuleInit() {
|
|
31
|
+
this.logger.log('Connecting to database...');
|
|
32
|
+
await this.$connect();
|
|
33
|
+
this.logger.log('Database connected');
|
|
34
|
+
|
|
35
|
+
// Seed default data on first run
|
|
36
|
+
await this.seedDefaultData();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Seed default data if database is empty (first run only)
|
|
41
|
+
* Only seeds if no profiles exist - respects user data
|
|
42
|
+
*/
|
|
43
|
+
private async seedDefaultData() {
|
|
44
|
+
// Check if any profiles exist - if so, user has data, don't seed
|
|
45
|
+
const profileCount = await this.profile.count();
|
|
46
|
+
if (profileCount > 0) {
|
|
47
|
+
this.logger.debug('Database has existing data, skipping seed');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.logger.log('First run detected, seeding default data...');
|
|
52
|
+
|
|
53
|
+
// Create default profile
|
|
54
|
+
const defaultProfile = await this.profile.create({
|
|
55
|
+
data: {
|
|
56
|
+
name: 'default',
|
|
57
|
+
description: 'Default MCP profile for general use',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
this.logger.log(`Created default profile: ${defaultProfile.id}`);
|
|
61
|
+
|
|
62
|
+
// Create Context7 MCP server
|
|
63
|
+
const context7 = await this.mcpServer.create({
|
|
64
|
+
data: {
|
|
65
|
+
name: 'Context7',
|
|
66
|
+
type: 'remote_http',
|
|
67
|
+
config: JSON.stringify({ url: 'https://mcp.context7.com/mcp' }),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
this.logger.log(`Created Context7 MCP server: ${context7.id}`);
|
|
71
|
+
|
|
72
|
+
// Link Context7 to default profile
|
|
73
|
+
await this.profileMcpServer.create({
|
|
74
|
+
data: {
|
|
75
|
+
profileId: defaultProfile.id,
|
|
76
|
+
mcpServerId: context7.id,
|
|
77
|
+
order: 0,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
this.logger.log('Linked Context7 to default profile');
|
|
81
|
+
|
|
82
|
+
this.logger.log('Default data seeding complete');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async onModuleDestroy() {
|
|
86
|
+
this.logger.log('Disconnecting from database...');
|
|
87
|
+
await this.$disconnect();
|
|
88
|
+
this.logger.log('Database disconnected');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clean database (for testing only)
|
|
93
|
+
*/
|
|
94
|
+
async cleanDatabase() {
|
|
95
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
96
|
+
throw new Error('cleanDatabase can only be called in test environment');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const tablenames = await this.$queryRaw<Array<{ name: string }>>`
|
|
100
|
+
SELECT name FROM sqlite_master
|
|
101
|
+
WHERE type='table'
|
|
102
|
+
AND name NOT LIKE '_prisma%'
|
|
103
|
+
AND name NOT LIKE 'sqlite%'
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
for (const { name } of tablenames) {
|
|
107
|
+
await this.$executeRawUnsafe(`DELETE FROM "${name}"`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Controller
|
|
3
|
+
*
|
|
4
|
+
* REST API endpoints for debug log management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Controller, Delete, Get, HttpCode, HttpStatus, Query } from '@nestjs/common';
|
|
8
|
+
import { DebugService } from './debug.service.js';
|
|
9
|
+
|
|
10
|
+
@Controller('debug')
|
|
11
|
+
export class DebugController {
|
|
12
|
+
constructor(private readonly debugService: DebugService) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get debug logs with optional filters
|
|
16
|
+
*/
|
|
17
|
+
@Get('logs')
|
|
18
|
+
async getLogs(
|
|
19
|
+
@Query('profileId') profileId?: string,
|
|
20
|
+
@Query('mcpServerId') mcpServerId?: string,
|
|
21
|
+
@Query('status') status?: 'pending' | 'success' | 'error',
|
|
22
|
+
@Query('since') since?: string,
|
|
23
|
+
@Query('until') until?: string,
|
|
24
|
+
@Query('limit') limit?: string,
|
|
25
|
+
@Query('offset') offset?: string
|
|
26
|
+
) {
|
|
27
|
+
const filter = {
|
|
28
|
+
profileId,
|
|
29
|
+
mcpServerId,
|
|
30
|
+
status,
|
|
31
|
+
since: since ? new Date(since) : undefined,
|
|
32
|
+
until: until ? new Date(until) : undefined,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return this.debugService.getLogs(
|
|
36
|
+
filter,
|
|
37
|
+
limit ? Number.parseInt(limit, 10) : 100,
|
|
38
|
+
offset ? Number.parseInt(offset, 10) : 0
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clear debug logs
|
|
44
|
+
*/
|
|
45
|
+
@Delete('logs')
|
|
46
|
+
@HttpCode(HttpStatus.NO_CONTENT)
|
|
47
|
+
async clearLogs(
|
|
48
|
+
@Query('profileId') profileId?: string,
|
|
49
|
+
@Query('mcpServerId') mcpServerId?: string
|
|
50
|
+
) {
|
|
51
|
+
await this.debugService.clearLogs({ profileId, mcpServerId });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Module
|
|
3
|
+
*
|
|
4
|
+
* Debug logging endpoints for MCP traffic inspection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Module } from '@nestjs/common';
|
|
8
|
+
import { DebugController } from './debug.controller.js';
|
|
9
|
+
import { DebugService } from './debug.service.js';
|
|
10
|
+
|
|
11
|
+
@Module({
|
|
12
|
+
controllers: [DebugController],
|
|
13
|
+
providers: [DebugService],
|
|
14
|
+
exports: [DebugService],
|
|
15
|
+
})
|
|
16
|
+
export class DebugModule {}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Service
|
|
3
|
+
*
|
|
4
|
+
* Manages debug logs for MCP traffic inspection.
|
|
5
|
+
* The DebugLog model tracks MCP requests/responses, not general log levels.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Injectable } from '@nestjs/common';
|
|
9
|
+
import { PrismaService } from '../database/prisma.service.js';
|
|
10
|
+
|
|
11
|
+
interface CreateLogDto {
|
|
12
|
+
profileId?: string | null;
|
|
13
|
+
mcpServerId?: string | null;
|
|
14
|
+
requestType: string;
|
|
15
|
+
requestPayload: string;
|
|
16
|
+
responsePayload?: string | null;
|
|
17
|
+
status: 'pending' | 'success' | 'error';
|
|
18
|
+
errorMessage?: string | null;
|
|
19
|
+
durationMs?: number | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface LogFilter {
|
|
23
|
+
profileId?: string;
|
|
24
|
+
mcpServerId?: string;
|
|
25
|
+
status?: 'pending' | 'success' | 'error';
|
|
26
|
+
since?: Date;
|
|
27
|
+
until?: Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Injectable()
|
|
31
|
+
export class DebugService {
|
|
32
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a debug log entry for an MCP request
|
|
36
|
+
*/
|
|
37
|
+
async createLog(dto: CreateLogDto) {
|
|
38
|
+
return this.prisma.debugLog.create({
|
|
39
|
+
data: {
|
|
40
|
+
profileId: dto.profileId,
|
|
41
|
+
mcpServerId: dto.mcpServerId,
|
|
42
|
+
requestType: dto.requestType,
|
|
43
|
+
requestPayload: dto.requestPayload,
|
|
44
|
+
responsePayload: dto.responsePayload,
|
|
45
|
+
status: dto.status,
|
|
46
|
+
errorMessage: dto.errorMessage,
|
|
47
|
+
durationMs: dto.durationMs,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Update a debug log entry with response data
|
|
54
|
+
*/
|
|
55
|
+
async updateLog(
|
|
56
|
+
id: string,
|
|
57
|
+
data: {
|
|
58
|
+
responsePayload?: string;
|
|
59
|
+
status?: 'pending' | 'success' | 'error';
|
|
60
|
+
errorMessage?: string;
|
|
61
|
+
durationMs?: number;
|
|
62
|
+
mcpServerId?: string;
|
|
63
|
+
}
|
|
64
|
+
) {
|
|
65
|
+
return this.prisma.debugLog.update({
|
|
66
|
+
where: { id },
|
|
67
|
+
data,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get debug logs with optional filters
|
|
73
|
+
*/
|
|
74
|
+
async getLogs(filter?: LogFilter, limit = 100, offset = 0) {
|
|
75
|
+
const where: Record<string, unknown> = {};
|
|
76
|
+
|
|
77
|
+
if (filter?.profileId) {
|
|
78
|
+
where.profileId = filter.profileId;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (filter?.mcpServerId) {
|
|
82
|
+
where.mcpServerId = filter.mcpServerId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (filter?.status) {
|
|
86
|
+
where.status = filter.status;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (filter?.since || filter?.until) {
|
|
90
|
+
where.createdAt = {};
|
|
91
|
+
if (filter.since) {
|
|
92
|
+
(where.createdAt as Record<string, Date>).gte = filter.since;
|
|
93
|
+
}
|
|
94
|
+
if (filter.until) {
|
|
95
|
+
(where.createdAt as Record<string, Date>).lte = filter.until;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const [logs, total] = await Promise.all([
|
|
100
|
+
this.prisma.debugLog.findMany({
|
|
101
|
+
where,
|
|
102
|
+
orderBy: { createdAt: 'desc' },
|
|
103
|
+
take: limit,
|
|
104
|
+
skip: offset,
|
|
105
|
+
include: {
|
|
106
|
+
profile: {
|
|
107
|
+
select: { id: true, name: true },
|
|
108
|
+
},
|
|
109
|
+
mcpServer: {
|
|
110
|
+
select: { id: true, name: true },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
this.prisma.debugLog.count({ where }),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
logs,
|
|
119
|
+
total,
|
|
120
|
+
limit,
|
|
121
|
+
offset,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clear debug logs
|
|
127
|
+
*/
|
|
128
|
+
async clearLogs(options?: { profileId?: string; mcpServerId?: string }) {
|
|
129
|
+
const where: Record<string, string> = {};
|
|
130
|
+
|
|
131
|
+
if (options?.profileId) {
|
|
132
|
+
where.profileId = options.profileId;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (options?.mcpServerId) {
|
|
136
|
+
where.mcpServerId = options.mcpServerId;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await this.prisma.debugLog.deleteMany({
|
|
140
|
+
where: Object.keys(where).length > 0 ? where : undefined,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Controller
|
|
3
|
+
*
|
|
4
|
+
* Health check endpoints for monitoring and load balancers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Controller, Get } from '@nestjs/common';
|
|
8
|
+
import { PrismaService } from '../database/prisma.service.js';
|
|
9
|
+
|
|
10
|
+
@Controller('health')
|
|
11
|
+
export class HealthController {
|
|
12
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Basic liveness probe
|
|
16
|
+
*/
|
|
17
|
+
@Get()
|
|
18
|
+
async getHealth() {
|
|
19
|
+
return {
|
|
20
|
+
status: 'ok',
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Readiness probe with database check
|
|
27
|
+
*/
|
|
28
|
+
@Get('ready')
|
|
29
|
+
async getReadiness() {
|
|
30
|
+
try {
|
|
31
|
+
// Test database connectivity
|
|
32
|
+
await this.prisma.$queryRaw`SELECT 1`;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
status: 'ok',
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
database: 'connected',
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
status: 'error',
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
database: 'disconnected',
|
|
44
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Module
|
|
3
|
+
*
|
|
4
|
+
* Health check endpoints for monitoring.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Module } from '@nestjs/common';
|
|
8
|
+
import { HealthController } from './health.controller.js';
|
|
9
|
+
|
|
10
|
+
@Module({
|
|
11
|
+
controllers: [HealthController],
|
|
12
|
+
})
|
|
13
|
+
export class HealthModule {}
|