@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,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Service
|
|
3
|
+
*
|
|
4
|
+
* Business logic for MCP server management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { RemoteHttpMcpServer, RemoteSseMcpServer } from '@dxheroes/local-mcp-core';
|
|
8
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
9
|
+
import { PrismaService } from '../database/prisma.service.js';
|
|
10
|
+
import { DebugService } from '../debug/debug.service.js';
|
|
11
|
+
import type { CreateMcpServerDto } from './dto/create-mcp-server.dto.js';
|
|
12
|
+
import type { UpdateMcpServerDto } from './dto/update-mcp-server.dto.js';
|
|
13
|
+
import { McpRegistry } from './mcp-registry.js';
|
|
14
|
+
|
|
15
|
+
@Injectable()
|
|
16
|
+
export class McpService {
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly prisma: PrismaService,
|
|
19
|
+
private readonly registry: McpRegistry,
|
|
20
|
+
private readonly debugService: DebugService
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get all MCP servers
|
|
25
|
+
*/
|
|
26
|
+
async findAll() {
|
|
27
|
+
const servers = await this.prisma.mcpServer.findMany({
|
|
28
|
+
include: {
|
|
29
|
+
profiles: {
|
|
30
|
+
include: {
|
|
31
|
+
profile: true,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
orderBy: { name: 'asc' },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Enrich with metadata from registry for builtin servers
|
|
39
|
+
return servers.map((server) => {
|
|
40
|
+
const builtinId = this.getBuiltinId(server.config);
|
|
41
|
+
|
|
42
|
+
if (builtinId && this.registry.has(builtinId)) {
|
|
43
|
+
const metadata = this.registry.get(builtinId)?.metadata;
|
|
44
|
+
return {
|
|
45
|
+
...server,
|
|
46
|
+
metadata,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return server;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get a specific MCP server
|
|
56
|
+
*/
|
|
57
|
+
async findById(id: string) {
|
|
58
|
+
const server = await this.prisma.mcpServer.findUnique({
|
|
59
|
+
where: { id },
|
|
60
|
+
include: {
|
|
61
|
+
profiles: {
|
|
62
|
+
include: {
|
|
63
|
+
profile: true,
|
|
64
|
+
tools: true,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
oauthToken: true,
|
|
68
|
+
toolsCache: true,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!server) {
|
|
73
|
+
throw new NotFoundException(`MCP server ${id} not found`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Enrich with metadata from registry for builtin servers
|
|
77
|
+
const builtinId = this.getBuiltinId(server.config);
|
|
78
|
+
|
|
79
|
+
if (builtinId && this.registry.has(builtinId)) {
|
|
80
|
+
const metadata = this.registry.get(builtinId)?.metadata;
|
|
81
|
+
return {
|
|
82
|
+
...server,
|
|
83
|
+
metadata,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return server;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a new MCP server
|
|
92
|
+
*/
|
|
93
|
+
async create(dto: CreateMcpServerDto) {
|
|
94
|
+
return this.prisma.mcpServer.create({
|
|
95
|
+
data: {
|
|
96
|
+
name: dto.name,
|
|
97
|
+
type: dto.type,
|
|
98
|
+
config: JSON.stringify(dto.config || {}),
|
|
99
|
+
apiKeyConfig: dto.apiKeyConfig ? JSON.stringify(dto.apiKeyConfig) : null,
|
|
100
|
+
oauthConfig: dto.oauthConfig ? JSON.stringify(dto.oauthConfig) : null,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Update an MCP server
|
|
107
|
+
*/
|
|
108
|
+
async update(id: string, dto: UpdateMcpServerDto) {
|
|
109
|
+
const server = await this.prisma.mcpServer.findUnique({ where: { id } });
|
|
110
|
+
|
|
111
|
+
if (!server) {
|
|
112
|
+
throw new NotFoundException(`MCP server ${id} not found`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return this.prisma.mcpServer.update({
|
|
116
|
+
where: { id },
|
|
117
|
+
data: {
|
|
118
|
+
name: dto.name,
|
|
119
|
+
type: dto.type,
|
|
120
|
+
config:
|
|
121
|
+
dto.config !== undefined
|
|
122
|
+
? typeof dto.config === 'string'
|
|
123
|
+
? dto.config
|
|
124
|
+
: JSON.stringify(dto.config)
|
|
125
|
+
: undefined,
|
|
126
|
+
apiKeyConfig: dto.apiKeyConfig !== undefined ? JSON.stringify(dto.apiKeyConfig) : undefined,
|
|
127
|
+
oauthConfig: dto.oauthConfig !== undefined ? JSON.stringify(dto.oauthConfig) : undefined,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delete an MCP server
|
|
134
|
+
*/
|
|
135
|
+
async delete(id: string) {
|
|
136
|
+
const server = await this.prisma.mcpServer.findUnique({ where: { id } });
|
|
137
|
+
|
|
138
|
+
if (!server) {
|
|
139
|
+
throw new NotFoundException(`MCP server ${id} not found`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await this.prisma.mcpServer.delete({ where: { id } });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get tools from an MCP server
|
|
147
|
+
*/
|
|
148
|
+
async getTools(id: string) {
|
|
149
|
+
const startTime = Date.now();
|
|
150
|
+
|
|
151
|
+
// Create debug log entry
|
|
152
|
+
const log = await this.debugService.createLog({
|
|
153
|
+
mcpServerId: id,
|
|
154
|
+
requestType: 'tools/list',
|
|
155
|
+
requestPayload: JSON.stringify({ method: 'tools/list', serverId: id }),
|
|
156
|
+
status: 'pending',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const result = await this.getToolsInternal(id);
|
|
161
|
+
|
|
162
|
+
// Update debug log with success
|
|
163
|
+
await this.debugService.updateLog(log.id, {
|
|
164
|
+
responsePayload: JSON.stringify(result),
|
|
165
|
+
status: 'success',
|
|
166
|
+
durationMs: Date.now() - startTime,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
// Update debug log with error
|
|
172
|
+
await this.debugService.updateLog(log.id, {
|
|
173
|
+
status: 'error',
|
|
174
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
|
175
|
+
durationMs: Date.now() - startTime,
|
|
176
|
+
});
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Internal method to get tools from an MCP server
|
|
183
|
+
*/
|
|
184
|
+
private async getToolsInternal(id: string) {
|
|
185
|
+
const server = await this.findById(id);
|
|
186
|
+
const builtinId = this.getBuiltinId(server.config);
|
|
187
|
+
|
|
188
|
+
// For builtin servers, get tools from the registry
|
|
189
|
+
if (builtinId && this.registry.has(builtinId)) {
|
|
190
|
+
const pkg = this.registry.get(builtinId);
|
|
191
|
+
if (pkg) {
|
|
192
|
+
// Get API key config if set
|
|
193
|
+
const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;
|
|
194
|
+
|
|
195
|
+
// Create server instance and list tools
|
|
196
|
+
const instance = pkg.createServer(apiKeyConfig);
|
|
197
|
+
await instance.initialize();
|
|
198
|
+
const tools = await instance.listTools();
|
|
199
|
+
return { tools };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// For remote_http servers, connect and fetch tools
|
|
204
|
+
if (server.type === 'remote_http') {
|
|
205
|
+
const config = this.parseConfig(server.config) as { url: string };
|
|
206
|
+
const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;
|
|
207
|
+
|
|
208
|
+
const remoteServer = new RemoteHttpMcpServer(
|
|
209
|
+
{ url: config.url, transport: 'http' },
|
|
210
|
+
null,
|
|
211
|
+
apiKeyConfig
|
|
212
|
+
);
|
|
213
|
+
await remoteServer.initialize();
|
|
214
|
+
const tools = await remoteServer.listTools();
|
|
215
|
+
return { tools };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// For remote_sse servers, connect and fetch tools
|
|
219
|
+
if (server.type === 'remote_sse') {
|
|
220
|
+
const config = this.parseConfig(server.config) as { url: string };
|
|
221
|
+
const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;
|
|
222
|
+
|
|
223
|
+
const remoteServer = new RemoteSseMcpServer(
|
|
224
|
+
{ url: config.url, transport: 'sse' },
|
|
225
|
+
null,
|
|
226
|
+
apiKeyConfig
|
|
227
|
+
);
|
|
228
|
+
await remoteServer.initialize();
|
|
229
|
+
const tools = await remoteServer.listTools();
|
|
230
|
+
return { tools };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// For cached tools from external servers (fallback)
|
|
234
|
+
const tools = await this.prisma.mcpServerToolsCache.findMany({
|
|
235
|
+
where: { mcpServerId: id },
|
|
236
|
+
});
|
|
237
|
+
return { tools };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get MCP server status with real validation
|
|
242
|
+
*/
|
|
243
|
+
async getStatus(id: string) {
|
|
244
|
+
const startTime = Date.now();
|
|
245
|
+
|
|
246
|
+
// Create debug log entry
|
|
247
|
+
const log = await this.debugService.createLog({
|
|
248
|
+
mcpServerId: id,
|
|
249
|
+
requestType: 'status/check',
|
|
250
|
+
requestPayload: JSON.stringify({ method: 'status/check', serverId: id }),
|
|
251
|
+
status: 'pending',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const result = await this.getStatusInternal(id);
|
|
256
|
+
|
|
257
|
+
// Update debug log with success or error based on status
|
|
258
|
+
await this.debugService.updateLog(log.id, {
|
|
259
|
+
responsePayload: JSON.stringify(result),
|
|
260
|
+
status: result.status === 'error' ? 'error' : 'success',
|
|
261
|
+
errorMessage: result.error,
|
|
262
|
+
durationMs: Date.now() - startTime,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return result;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// Update debug log with error
|
|
268
|
+
await this.debugService.updateLog(log.id, {
|
|
269
|
+
status: 'error',
|
|
270
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
|
271
|
+
durationMs: Date.now() - startTime,
|
|
272
|
+
});
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Internal method to get MCP server status with real validation
|
|
279
|
+
*/
|
|
280
|
+
private async getStatusInternal(id: string) {
|
|
281
|
+
const server = await this.findById(id);
|
|
282
|
+
const builtinId = this.getBuiltinId(server.config);
|
|
283
|
+
|
|
284
|
+
// Check if it's a builtin server
|
|
285
|
+
const isBuiltin = builtinId && this.registry.has(builtinId);
|
|
286
|
+
|
|
287
|
+
// Check API key config
|
|
288
|
+
const hasApiKey = !!server.apiKeyConfig;
|
|
289
|
+
|
|
290
|
+
// Check OAuth token
|
|
291
|
+
const hasOAuth = !!server.oauthToken;
|
|
292
|
+
|
|
293
|
+
// Get metadata
|
|
294
|
+
const metadata = isBuiltin ? this.registry.get(builtinId)?.metadata : null;
|
|
295
|
+
const requiresApiKey = metadata?.requiresApiKey ?? false;
|
|
296
|
+
const requiresOAuth = metadata?.requiresOAuth ?? false;
|
|
297
|
+
|
|
298
|
+
// Default status
|
|
299
|
+
let status: 'connected' | 'error' | 'unknown' = 'unknown';
|
|
300
|
+
let validationError: string | undefined;
|
|
301
|
+
let validationDetails: string | undefined;
|
|
302
|
+
|
|
303
|
+
// For builtin servers with API key, actually validate
|
|
304
|
+
if (isBuiltin && hasApiKey) {
|
|
305
|
+
const pkg = this.registry.get(builtinId);
|
|
306
|
+
if (pkg) {
|
|
307
|
+
const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const instance = pkg.createServer(apiKeyConfig);
|
|
311
|
+
// Call validate() method
|
|
312
|
+
const validation = await instance.validate();
|
|
313
|
+
status = validation.valid ? 'connected' : 'error';
|
|
314
|
+
validationError = validation.error;
|
|
315
|
+
validationDetails = validation.valid
|
|
316
|
+
? 'API key validated successfully'
|
|
317
|
+
: `Validation failed: ${validation.error}`;
|
|
318
|
+
console.log(`[McpService] Validation result for ${server.name}:`, validation);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
status = 'error';
|
|
321
|
+
validationError = error instanceof Error ? error.message : 'Unknown error';
|
|
322
|
+
validationDetails = `Connection test failed: ${validationError}`;
|
|
323
|
+
console.error(`[McpService] Validation error for ${server.name}:`, error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} else if (!hasApiKey && requiresApiKey) {
|
|
327
|
+
status = 'error';
|
|
328
|
+
validationError = 'API key required';
|
|
329
|
+
validationDetails = 'This server requires an API key to function';
|
|
330
|
+
} else if (isBuiltin && !requiresApiKey) {
|
|
331
|
+
status = 'connected';
|
|
332
|
+
validationDetails = 'Server ready (no API key required)';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// For remote_http servers, validate by connecting
|
|
336
|
+
if (server.type === 'remote_http' && status === 'unknown') {
|
|
337
|
+
const config = this.parseConfig(server.config) as { url: string };
|
|
338
|
+
const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const remoteServer = new RemoteHttpMcpServer(
|
|
342
|
+
{ url: config.url, transport: 'http' },
|
|
343
|
+
null,
|
|
344
|
+
apiKeyConfig
|
|
345
|
+
);
|
|
346
|
+
await remoteServer.initialize();
|
|
347
|
+
const tools = await remoteServer.listTools();
|
|
348
|
+
status = 'connected';
|
|
349
|
+
validationDetails = `Connected successfully. ${tools.length} tools available.`;
|
|
350
|
+
console.log(
|
|
351
|
+
`[McpService] Remote HTTP validation for ${server.name}: ${tools.length} tools`
|
|
352
|
+
);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
status = 'error';
|
|
355
|
+
validationError = error instanceof Error ? error.message : 'Unknown error';
|
|
356
|
+
validationDetails = `Connection failed: ${validationError}`;
|
|
357
|
+
console.error(`[McpService] Remote HTTP validation error for ${server.name}:`, error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// For remote_sse servers, validate by connecting
|
|
362
|
+
if (server.type === 'remote_sse' && status === 'unknown') {
|
|
363
|
+
const config = this.parseConfig(server.config) as { url: string };
|
|
364
|
+
const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig as string) : null;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const remoteServer = new RemoteSseMcpServer(
|
|
368
|
+
{ url: config.url, transport: 'sse' },
|
|
369
|
+
null,
|
|
370
|
+
apiKeyConfig
|
|
371
|
+
);
|
|
372
|
+
await remoteServer.initialize();
|
|
373
|
+
const tools = await remoteServer.listTools();
|
|
374
|
+
status = 'connected';
|
|
375
|
+
validationDetails = `Connected successfully (SSE). ${tools.length} tools available.`;
|
|
376
|
+
console.log(`[McpService] Remote SSE validation for ${server.name}: ${tools.length} tools`);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
status = 'error';
|
|
379
|
+
validationError = error instanceof Error ? error.message : 'Unknown error';
|
|
380
|
+
validationDetails = `Connection failed: ${validationError}`;
|
|
381
|
+
console.error(`[McpService] Remote SSE validation error for ${server.name}:`, error);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const isReady = status === 'connected';
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
id: server.id,
|
|
389
|
+
name: server.name,
|
|
390
|
+
type: server.type,
|
|
391
|
+
isBuiltin,
|
|
392
|
+
hasApiKey,
|
|
393
|
+
hasOAuth,
|
|
394
|
+
requiresApiKey,
|
|
395
|
+
requiresOAuth,
|
|
396
|
+
isReady,
|
|
397
|
+
status,
|
|
398
|
+
error: validationError,
|
|
399
|
+
details: validationDetails,
|
|
400
|
+
validatedAt: new Date().toISOString(),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Extract builtinId from config (handles JSON string parsing)
|
|
406
|
+
*/
|
|
407
|
+
private getBuiltinId(config: unknown): string | null {
|
|
408
|
+
const parsed = this.parseConfig(config);
|
|
409
|
+
const builtinId = parsed?.builtinId;
|
|
410
|
+
|
|
411
|
+
if (typeof builtinId === 'string') {
|
|
412
|
+
return builtinId;
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private parseConfig(config: unknown): Record<string, unknown> | null {
|
|
418
|
+
if (typeof config === 'string') {
|
|
419
|
+
try {
|
|
420
|
+
return JSON.parse(config);
|
|
421
|
+
} catch {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return config as Record<string, unknown> | null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Controller
|
|
3
|
+
*
|
|
4
|
+
* REST API endpoints for OAuth token management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
|
|
8
|
+
import { OAuthService } from './oauth.service.js';
|
|
9
|
+
|
|
10
|
+
interface StartOAuthDto {
|
|
11
|
+
mcpServerId: string;
|
|
12
|
+
authorizationServerUrl: string;
|
|
13
|
+
clientId: string;
|
|
14
|
+
scopes?: string[];
|
|
15
|
+
redirectUri: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CompleteOAuthDto {
|
|
19
|
+
mcpServerId: string;
|
|
20
|
+
code: string;
|
|
21
|
+
codeVerifier: string;
|
|
22
|
+
tokenEndpoint: string;
|
|
23
|
+
clientId: string;
|
|
24
|
+
redirectUri: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface StoreTokenDto {
|
|
28
|
+
accessToken: string;
|
|
29
|
+
refreshToken?: string | null;
|
|
30
|
+
tokenType?: string;
|
|
31
|
+
scope?: string | null;
|
|
32
|
+
expiresIn?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Controller('oauth')
|
|
36
|
+
export class OAuthController {
|
|
37
|
+
constructor(private readonly oauthService: OAuthService) {}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Start OAuth flow for an MCP server
|
|
41
|
+
*/
|
|
42
|
+
@Post('start')
|
|
43
|
+
async startOAuthFlow(@Body() dto: StartOAuthDto) {
|
|
44
|
+
return this.oauthService.startOAuthFlow(dto);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Complete OAuth flow with authorization code
|
|
49
|
+
*/
|
|
50
|
+
@Post('complete')
|
|
51
|
+
async completeOAuthFlow(@Body() dto: CompleteOAuthDto) {
|
|
52
|
+
return this.oauthService.completeOAuthFlow(dto);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Manually store a token for an MCP server
|
|
57
|
+
*/
|
|
58
|
+
@Post('servers/:serverId/token')
|
|
59
|
+
@HttpCode(HttpStatus.CREATED)
|
|
60
|
+
async storeToken(@Param('serverId') serverId: string, @Body() dto: StoreTokenDto) {
|
|
61
|
+
const expiresAt = dto.expiresIn ? new Date(Date.now() + dto.expiresIn * 1000) : null;
|
|
62
|
+
|
|
63
|
+
return this.oauthService.storeToken({
|
|
64
|
+
mcpServerId: serverId,
|
|
65
|
+
accessToken: dto.accessToken,
|
|
66
|
+
refreshToken: dto.refreshToken,
|
|
67
|
+
tokenType: dto.tokenType || 'Bearer',
|
|
68
|
+
scope: dto.scope,
|
|
69
|
+
expiresAt,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get OAuth token for an MCP server
|
|
75
|
+
*/
|
|
76
|
+
@Get('servers/:serverId/token')
|
|
77
|
+
async getToken(@Param('serverId') serverId: string) {
|
|
78
|
+
return this.oauthService.getToken(serverId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Delete OAuth token for an MCP server
|
|
83
|
+
*/
|
|
84
|
+
@Delete('servers/:serverId/token')
|
|
85
|
+
@HttpCode(HttpStatus.NO_CONTENT)
|
|
86
|
+
async deleteToken(@Param('serverId') serverId: string) {
|
|
87
|
+
await this.oauthService.deleteToken(serverId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Module
|
|
3
|
+
*
|
|
4
|
+
* Handles OAuth flows for MCP servers that require OAuth authentication.
|
|
5
|
+
* This is NOT for user authentication - the app has no user auth.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Module } from '@nestjs/common';
|
|
9
|
+
import { OAuthController } from './oauth.controller.js';
|
|
10
|
+
import { OAuthService } from './oauth.service.js';
|
|
11
|
+
|
|
12
|
+
@Module({
|
|
13
|
+
controllers: [OAuthController],
|
|
14
|
+
providers: [OAuthService],
|
|
15
|
+
exports: [OAuthService],
|
|
16
|
+
})
|
|
17
|
+
export class OAuthModule {}
|