@dxheroes/local-mcp-backend 0.12.0 → 0.13.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 +22 -0
- package/dist/__tests__/unit/mcp-server-tool-configs.test.js +286 -0
- package/dist/__tests__/unit/mcp-server-tool-configs.test.js.map +1 -0
- package/dist/__tests__/unit/proxy-tool-filter.test.js +351 -0
- package/dist/__tests__/unit/proxy-tool-filter.test.js.map +1 -0
- package/dist/modules/mcp/mcp.controller.js +38 -0
- package/dist/modules/mcp/mcp.controller.js.map +1 -1
- package/dist/modules/mcp/mcp.service.js +107 -0
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/proxy/proxy.service.js +34 -4
- package/dist/modules/proxy/proxy.service.js.map +1 -1
- package/package.json +9 -9
- package/src/__tests__/unit/mcp-server-tool-configs.test.ts +254 -0
- package/src/__tests__/unit/proxy-tool-filter.test.ts +275 -0
- package/src/modules/mcp/mcp.controller.ts +25 -0
- package/src/modules/mcp/mcp.service.ts +114 -0
- package/src/modules/proxy/proxy.service.ts +52 -8
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
> @dxheroes/local-mcp-backend@0.
|
|
2
|
+
> @dxheroes/local-mcp-backend@0.13.0 build /home/runner/work/local-mcp-gateway/local-mcp-gateway/apps/backend
|
|
3
3
|
> nest build
|
|
4
4
|
|
|
5
5
|
- [46m[1m TSC [22m[49m[36m Initializing type checker...[39m
|
|
6
6
|
✔ [46m[1m TSC [22m[49m[36m Initializing type checker...[39m
|
|
7
7
|
[32m> [39m[42m[1m TSC [22m[49m[32m Found 0 issues.[39m
|
|
8
8
|
[36m> [39m[46m[1m SWC [22m[49m [36mRunning...[39m
|
|
9
|
-
Successfully compiled:
|
|
9
|
+
Successfully compiled: 70 files with swc (113.12ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.0](https://github.com/DXHeroes/local-mcp-gateway/compare/backend-v0.12.0...backend-v0.13.0) (2026-03-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **mcp:** implement server-level tool configuration management ([f8a1f58](https://github.com/DXHeroes/local-mcp-gateway/commit/f8a1f58d28c4da9a8a05034b1665ba2f58338d01))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @dxheroes/local-mcp-core bumped to 0.10.0
|
|
16
|
+
* @dxheroes/local-mcp-database bumped to 0.7.0
|
|
17
|
+
* @dxheroes/mcp-abra-flexi bumped to 0.4.1
|
|
18
|
+
* @dxheroes/mcp-fakturoid bumped to 0.4.1
|
|
19
|
+
* @dxheroes/mcp-gemini-deep-research bumped to 0.5.9
|
|
20
|
+
* @dxheroes/mcp-merk bumped to 0.3.9
|
|
21
|
+
* @dxheroes/mcp-toggl bumped to 0.3.9
|
|
22
|
+
* devDependencies
|
|
23
|
+
* @dxheroes/local-mcp-config bumped to 0.4.14
|
|
24
|
+
|
|
3
25
|
## [0.12.0](https://github.com/DXHeroes/local-mcp-gateway/compare/backend-v0.11.0...backend-v0.12.0) (2026-03-16)
|
|
4
26
|
|
|
5
27
|
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for McpService — server-level tool configurations (allowlist)
|
|
3
|
+
*/ import { ForbiddenException } from "@nestjs/common";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { McpService } from "../../modules/mcp/mcp.service.js";
|
|
6
|
+
import { McpRegistry } from "../../modules/mcp/mcp-registry.js";
|
|
7
|
+
// Mock @dxheroes/local-mcp-core
|
|
8
|
+
const mockInitialize = vi.fn().mockResolvedValue(undefined);
|
|
9
|
+
const mockListTools = vi.fn().mockResolvedValue([
|
|
10
|
+
{
|
|
11
|
+
name: 'read_file',
|
|
12
|
+
description: 'Read a file'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'write_file',
|
|
16
|
+
description: 'Write a file'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'delete_file',
|
|
20
|
+
description: 'Delete a file'
|
|
21
|
+
}
|
|
22
|
+
]);
|
|
23
|
+
const mockShutdown = vi.fn().mockResolvedValue(undefined);
|
|
24
|
+
vi.mock('@dxheroes/local-mcp-core', async (importOriginal)=>{
|
|
25
|
+
const actual = await importOriginal();
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
ExternalMcpServer: class MockExternalMcpServer {
|
|
29
|
+
constructor(config){
|
|
30
|
+
this.initialize = mockInitialize;
|
|
31
|
+
this.listTools = mockListTools;
|
|
32
|
+
this.shutdown = mockShutdown;
|
|
33
|
+
this.config = config;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
RemoteHttpMcpServer: class MockRemoteHttpMcpServer {
|
|
37
|
+
constructor(config){
|
|
38
|
+
this.initialize = mockInitialize;
|
|
39
|
+
this.listTools = mockListTools;
|
|
40
|
+
this.config = config;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
RemoteSseMcpServer: class MockRemoteSseMcpServer {
|
|
44
|
+
constructor(config){
|
|
45
|
+
this.initialize = mockInitialize;
|
|
46
|
+
this.listTools = mockListTools;
|
|
47
|
+
this.config = config;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
describe('McpService — Tool Configs', ()=>{
|
|
53
|
+
let service;
|
|
54
|
+
// biome-ignore lint: test mock
|
|
55
|
+
let prisma;
|
|
56
|
+
let registry;
|
|
57
|
+
let debugService;
|
|
58
|
+
let sharingService;
|
|
59
|
+
const server = {
|
|
60
|
+
id: 'srv-1',
|
|
61
|
+
name: 'Test Server',
|
|
62
|
+
type: 'remote_http',
|
|
63
|
+
config: '{"url":"https://example.com/mcp"}',
|
|
64
|
+
apiKeyConfig: null,
|
|
65
|
+
oauthConfig: null,
|
|
66
|
+
userId: 'user-a',
|
|
67
|
+
profiles: [],
|
|
68
|
+
oauthToken: null,
|
|
69
|
+
toolsCache: []
|
|
70
|
+
};
|
|
71
|
+
beforeEach(()=>{
|
|
72
|
+
prisma = {
|
|
73
|
+
mcpServer: {
|
|
74
|
+
findMany: vi.fn().mockResolvedValue([]),
|
|
75
|
+
findUnique: vi.fn().mockResolvedValue(null),
|
|
76
|
+
create: vi.fn().mockResolvedValue({
|
|
77
|
+
id: 'new-1'
|
|
78
|
+
}),
|
|
79
|
+
update: vi.fn().mockResolvedValue({}),
|
|
80
|
+
delete: vi.fn().mockResolvedValue(undefined)
|
|
81
|
+
},
|
|
82
|
+
member: {
|
|
83
|
+
findMany: vi.fn().mockResolvedValue([])
|
|
84
|
+
},
|
|
85
|
+
mcpServerToolsCache: {
|
|
86
|
+
findMany: vi.fn().mockResolvedValue([])
|
|
87
|
+
},
|
|
88
|
+
mcpServerToolConfig: {
|
|
89
|
+
findMany: vi.fn().mockResolvedValue([]),
|
|
90
|
+
deleteMany: vi.fn().mockResolvedValue({
|
|
91
|
+
count: 0
|
|
92
|
+
}),
|
|
93
|
+
createMany: vi.fn().mockResolvedValue({
|
|
94
|
+
count: 0
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
$transaction: vi.fn(async (fn)=>{
|
|
98
|
+
return fn(prisma);
|
|
99
|
+
})
|
|
100
|
+
};
|
|
101
|
+
registry = new McpRegistry();
|
|
102
|
+
debugService = {
|
|
103
|
+
createLog: vi.fn().mockResolvedValue({
|
|
104
|
+
id: 'log-1'
|
|
105
|
+
}),
|
|
106
|
+
updateLog: vi.fn().mockResolvedValue({})
|
|
107
|
+
};
|
|
108
|
+
sharingService = {
|
|
109
|
+
getSharedResourceIds: vi.fn().mockResolvedValue([]),
|
|
110
|
+
isSharedWith: vi.fn().mockResolvedValue(false),
|
|
111
|
+
getPermission: vi.fn().mockResolvedValue(null)
|
|
112
|
+
};
|
|
113
|
+
service = new McpService(prisma, registry, debugService, sharingService);
|
|
114
|
+
});
|
|
115
|
+
describe('getServerToolConfigs', ()=>{
|
|
116
|
+
it('returns all tools as enabled when no configs exist (unconfigured server)', async ()=>{
|
|
117
|
+
prisma.mcpServer.findUnique.mockResolvedValue(server);
|
|
118
|
+
prisma.mcpServerToolConfig.findMany.mockResolvedValue([]);
|
|
119
|
+
const result = await service.getServerToolConfigs('srv-1', 'user-a');
|
|
120
|
+
expect(result.hasConfigs).toBe(false);
|
|
121
|
+
expect(result.tools).toHaveLength(3);
|
|
122
|
+
expect(result.tools.every((t)=>t.isEnabled)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
it('returns tools with config records using their isEnabled value', async ()=>{
|
|
125
|
+
prisma.mcpServer.findUnique.mockResolvedValue(server);
|
|
126
|
+
prisma.mcpServerToolConfig.findMany.mockResolvedValue([
|
|
127
|
+
{
|
|
128
|
+
id: 'tc-1',
|
|
129
|
+
mcpServerId: 'srv-1',
|
|
130
|
+
toolName: 'read_file',
|
|
131
|
+
isEnabled: true
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'tc-2',
|
|
135
|
+
mcpServerId: 'srv-1',
|
|
136
|
+
toolName: 'write_file',
|
|
137
|
+
isEnabled: false
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'tc-3',
|
|
141
|
+
mcpServerId: 'srv-1',
|
|
142
|
+
toolName: 'delete_file',
|
|
143
|
+
isEnabled: false
|
|
144
|
+
}
|
|
145
|
+
]);
|
|
146
|
+
const result = await service.getServerToolConfigs('srv-1', 'user-a');
|
|
147
|
+
expect(result.hasConfigs).toBe(true);
|
|
148
|
+
expect(result.tools).toHaveLength(3);
|
|
149
|
+
const readFile = result.tools.find((t)=>t.name === 'read_file');
|
|
150
|
+
const writeFile = result.tools.find((t)=>t.name === 'write_file');
|
|
151
|
+
const deleteFile = result.tools.find((t)=>t.name === 'delete_file');
|
|
152
|
+
expect(readFile?.isEnabled).toBe(true);
|
|
153
|
+
expect(writeFile?.isEnabled).toBe(false);
|
|
154
|
+
expect(deleteFile?.isEnabled).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
it('treats new tools (no config record) as disabled when configs exist', async ()=>{
|
|
157
|
+
prisma.mcpServer.findUnique.mockResolvedValue(server);
|
|
158
|
+
// Only read_file has a config — write_file and delete_file are "new"
|
|
159
|
+
prisma.mcpServerToolConfig.findMany.mockResolvedValue([
|
|
160
|
+
{
|
|
161
|
+
id: 'tc-1',
|
|
162
|
+
mcpServerId: 'srv-1',
|
|
163
|
+
toolName: 'read_file',
|
|
164
|
+
isEnabled: true
|
|
165
|
+
}
|
|
166
|
+
]);
|
|
167
|
+
const result = await service.getServerToolConfigs('srv-1', 'user-a');
|
|
168
|
+
expect(result.hasConfigs).toBe(true);
|
|
169
|
+
const readFile = result.tools.find((t)=>t.name === 'read_file');
|
|
170
|
+
const writeFile = result.tools.find((t)=>t.name === 'write_file');
|
|
171
|
+
const deleteFile = result.tools.find((t)=>t.name === 'delete_file');
|
|
172
|
+
expect(readFile?.isEnabled).toBe(true);
|
|
173
|
+
expect(readFile?.hasConfig).toBe(true);
|
|
174
|
+
expect(writeFile?.isEnabled).toBe(false);
|
|
175
|
+
expect(writeFile?.hasConfig).toBe(false);
|
|
176
|
+
expect(deleteFile?.isEnabled).toBe(false);
|
|
177
|
+
expect(deleteFile?.hasConfig).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
it('throws ForbiddenException for non-owner without sharing', async ()=>{
|
|
180
|
+
prisma.mcpServer.findUnique.mockResolvedValue(server);
|
|
181
|
+
await expect(service.getServerToolConfigs('srv-1', 'user-b')).rejects.toThrow(ForbiddenException);
|
|
182
|
+
});
|
|
183
|
+
it('allows non-owner with sharing to view configs', async ()=>{
|
|
184
|
+
prisma.mcpServer.findUnique.mockResolvedValue(server);
|
|
185
|
+
sharingService.isSharedWith.mockResolvedValue(true);
|
|
186
|
+
prisma.mcpServerToolConfig.findMany.mockResolvedValue([]);
|
|
187
|
+
const result = await service.getServerToolConfigs('srv-1', 'user-b');
|
|
188
|
+
expect(result.tools).toHaveLength(3);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe('updateServerToolConfigs', ()=>{
|
|
192
|
+
it('creates config records for all tools in a transaction', async ()=>{
|
|
193
|
+
prisma.mcpServer.findUnique.mockResolvedValue(server);
|
|
194
|
+
prisma.mcpServerToolConfig.findMany.mockResolvedValue([
|
|
195
|
+
{
|
|
196
|
+
id: 'tc-1',
|
|
197
|
+
mcpServerId: 'srv-1',
|
|
198
|
+
toolName: 'read_file',
|
|
199
|
+
isEnabled: true
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'tc-2',
|
|
203
|
+
mcpServerId: 'srv-1',
|
|
204
|
+
toolName: 'write_file',
|
|
205
|
+
isEnabled: false
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 'tc-3',
|
|
209
|
+
mcpServerId: 'srv-1',
|
|
210
|
+
toolName: 'delete_file',
|
|
211
|
+
isEnabled: false
|
|
212
|
+
}
|
|
213
|
+
]);
|
|
214
|
+
await service.updateServerToolConfigs('srv-1', [
|
|
215
|
+
{
|
|
216
|
+
toolName: 'read_file',
|
|
217
|
+
isEnabled: true
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
toolName: 'write_file',
|
|
221
|
+
isEnabled: false
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
toolName: 'delete_file',
|
|
225
|
+
isEnabled: false
|
|
226
|
+
}
|
|
227
|
+
], 'user-a');
|
|
228
|
+
expect(prisma.$transaction).toHaveBeenCalled();
|
|
229
|
+
expect(prisma.mcpServerToolConfig.deleteMany).toHaveBeenCalledWith({
|
|
230
|
+
where: {
|
|
231
|
+
mcpServerId: 'srv-1'
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
expect(prisma.mcpServerToolConfig.createMany).toHaveBeenCalledWith({
|
|
235
|
+
data: [
|
|
236
|
+
{
|
|
237
|
+
mcpServerId: 'srv-1',
|
|
238
|
+
toolName: 'read_file',
|
|
239
|
+
isEnabled: true
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
mcpServerId: 'srv-1',
|
|
243
|
+
toolName: 'write_file',
|
|
244
|
+
isEnabled: false
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
mcpServerId: 'srv-1',
|
|
248
|
+
toolName: 'delete_file',
|
|
249
|
+
isEnabled: false
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
it('throws ForbiddenException for non-owner', async ()=>{
|
|
255
|
+
prisma.mcpServer.findUnique.mockResolvedValue(server);
|
|
256
|
+
await expect(service.updateServerToolConfigs('srv-1', [
|
|
257
|
+
{
|
|
258
|
+
toolName: 'read_file',
|
|
259
|
+
isEnabled: true
|
|
260
|
+
}
|
|
261
|
+
], 'user-b')).rejects.toThrow(ForbiddenException);
|
|
262
|
+
});
|
|
263
|
+
it('allows admin shared user to update configs', async ()=>{
|
|
264
|
+
prisma.mcpServer.findUnique.mockResolvedValue(server);
|
|
265
|
+
sharingService.getPermission.mockResolvedValue('admin');
|
|
266
|
+
sharingService.isSharedWith.mockResolvedValue(true);
|
|
267
|
+
prisma.mcpServerToolConfig.findMany.mockResolvedValue([
|
|
268
|
+
{
|
|
269
|
+
id: 'tc-1',
|
|
270
|
+
mcpServerId: 'srv-1',
|
|
271
|
+
toolName: 'read_file',
|
|
272
|
+
isEnabled: true
|
|
273
|
+
}
|
|
274
|
+
]);
|
|
275
|
+
const result = await service.updateServerToolConfigs('srv-1', [
|
|
276
|
+
{
|
|
277
|
+
toolName: 'read_file',
|
|
278
|
+
isEnabled: true
|
|
279
|
+
}
|
|
280
|
+
], 'user-b');
|
|
281
|
+
expect(result.tools).toBeDefined();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
//# sourceMappingURL=mcp-server-tool-configs.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/__tests__/unit/mcp-server-tool-configs.test.ts"],"sourcesContent":["/**\n * Tests for McpService — server-level tool configurations (allowlist)\n */\n\nimport { ForbiddenException } from '@nestjs/common';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport type { PrismaService } from '../../modules/database/prisma.service.js';\nimport type { DebugService } from '../../modules/debug/debug.service.js';\nimport { McpService } from '../../modules/mcp/mcp.service.js';\nimport { McpRegistry } from '../../modules/mcp/mcp-registry.js';\nimport type { SharingService } from '../../modules/sharing/sharing.service.js';\n\n// Mock @dxheroes/local-mcp-core\nconst mockInitialize = vi.fn().mockResolvedValue(undefined);\nconst mockListTools = vi.fn().mockResolvedValue([\n { name: 'read_file', description: 'Read a file' },\n { name: 'write_file', description: 'Write a file' },\n { name: 'delete_file', description: 'Delete a file' },\n]);\nconst mockShutdown = vi.fn().mockResolvedValue(undefined);\n\nvi.mock('@dxheroes/local-mcp-core', async (importOriginal) => {\n const actual = await importOriginal<typeof import('@dxheroes/local-mcp-core')>();\n return {\n ...actual,\n ExternalMcpServer: class MockExternalMcpServer {\n config: unknown;\n constructor(config: unknown) {\n this.config = config;\n }\n initialize = mockInitialize;\n listTools = mockListTools;\n shutdown = mockShutdown;\n },\n RemoteHttpMcpServer: class MockRemoteHttpMcpServer {\n config: unknown;\n constructor(config: unknown) {\n this.config = config;\n }\n initialize = mockInitialize;\n listTools = mockListTools;\n },\n RemoteSseMcpServer: class MockRemoteSseMcpServer {\n config: unknown;\n constructor(config: unknown) {\n this.config = config;\n }\n initialize = mockInitialize;\n listTools = mockListTools;\n },\n };\n});\n\ndescribe('McpService — Tool Configs', () => {\n let service: McpService;\n // biome-ignore lint: test mock\n let prisma: any;\n let registry: McpRegistry;\n let debugService: Record<string, ReturnType<typeof vi.fn>>;\n let sharingService: Record<string, ReturnType<typeof vi.fn>>;\n\n const server = {\n id: 'srv-1',\n name: 'Test Server',\n type: 'remote_http',\n config: '{\"url\":\"https://example.com/mcp\"}',\n apiKeyConfig: null,\n oauthConfig: null,\n userId: 'user-a',\n profiles: [],\n oauthToken: null,\n toolsCache: [],\n };\n\n beforeEach(() => {\n prisma = {\n mcpServer: {\n findMany: vi.fn().mockResolvedValue([]),\n findUnique: vi.fn().mockResolvedValue(null),\n create: vi.fn().mockResolvedValue({ id: 'new-1' }),\n update: vi.fn().mockResolvedValue({}),\n delete: vi.fn().mockResolvedValue(undefined),\n },\n member: {\n findMany: vi.fn().mockResolvedValue([]),\n },\n mcpServerToolsCache: {\n findMany: vi.fn().mockResolvedValue([]),\n },\n mcpServerToolConfig: {\n findMany: vi.fn().mockResolvedValue([]),\n deleteMany: vi.fn().mockResolvedValue({ count: 0 }),\n createMany: vi.fn().mockResolvedValue({ count: 0 }),\n },\n $transaction: vi.fn(async (fn: (tx: unknown) => Promise<unknown>) => {\n return fn(prisma);\n }),\n };\n registry = new McpRegistry();\n debugService = {\n createLog: vi.fn().mockResolvedValue({ id: 'log-1' }),\n updateLog: vi.fn().mockResolvedValue({}),\n };\n sharingService = {\n getSharedResourceIds: vi.fn().mockResolvedValue([]),\n isSharedWith: vi.fn().mockResolvedValue(false),\n getPermission: vi.fn().mockResolvedValue(null),\n };\n service = new McpService(\n prisma as unknown as PrismaService,\n registry,\n debugService as unknown as DebugService,\n sharingService as unknown as SharingService\n );\n });\n\n describe('getServerToolConfigs', () => {\n it('returns all tools as enabled when no configs exist (unconfigured server)', async () => {\n prisma.mcpServer.findUnique.mockResolvedValue(server);\n prisma.mcpServerToolConfig.findMany.mockResolvedValue([]);\n\n const result = await service.getServerToolConfigs('srv-1', 'user-a');\n\n expect(result.hasConfigs).toBe(false);\n expect(result.tools).toHaveLength(3);\n expect(result.tools.every((t: { isEnabled: boolean }) => t.isEnabled)).toBe(true);\n });\n\n it('returns tools with config records using their isEnabled value', async () => {\n prisma.mcpServer.findUnique.mockResolvedValue(server);\n prisma.mcpServerToolConfig.findMany.mockResolvedValue([\n { id: 'tc-1', mcpServerId: 'srv-1', toolName: 'read_file', isEnabled: true },\n { id: 'tc-2', mcpServerId: 'srv-1', toolName: 'write_file', isEnabled: false },\n { id: 'tc-3', mcpServerId: 'srv-1', toolName: 'delete_file', isEnabled: false },\n ]);\n\n const result = await service.getServerToolConfigs('srv-1', 'user-a');\n\n expect(result.hasConfigs).toBe(true);\n expect(result.tools).toHaveLength(3);\n\n const readFile = result.tools.find((t: { name: string }) => t.name === 'read_file');\n const writeFile = result.tools.find((t: { name: string }) => t.name === 'write_file');\n const deleteFile = result.tools.find((t: { name: string }) => t.name === 'delete_file');\n\n expect(readFile?.isEnabled).toBe(true);\n expect(writeFile?.isEnabled).toBe(false);\n expect(deleteFile?.isEnabled).toBe(false);\n });\n\n it('treats new tools (no config record) as disabled when configs exist', async () => {\n prisma.mcpServer.findUnique.mockResolvedValue(server);\n // Only read_file has a config — write_file and delete_file are \"new\"\n prisma.mcpServerToolConfig.findMany.mockResolvedValue([\n { id: 'tc-1', mcpServerId: 'srv-1', toolName: 'read_file', isEnabled: true },\n ]);\n\n const result = await service.getServerToolConfigs('srv-1', 'user-a');\n\n expect(result.hasConfigs).toBe(true);\n\n const readFile = result.tools.find((t: { name: string }) => t.name === 'read_file');\n const writeFile = result.tools.find((t: { name: string }) => t.name === 'write_file');\n const deleteFile = result.tools.find((t: { name: string }) => t.name === 'delete_file');\n\n expect(readFile?.isEnabled).toBe(true);\n expect(readFile?.hasConfig).toBe(true);\n expect(writeFile?.isEnabled).toBe(false);\n expect(writeFile?.hasConfig).toBe(false);\n expect(deleteFile?.isEnabled).toBe(false);\n expect(deleteFile?.hasConfig).toBe(false);\n });\n\n it('throws ForbiddenException for non-owner without sharing', async () => {\n prisma.mcpServer.findUnique.mockResolvedValue(server);\n\n await expect(service.getServerToolConfigs('srv-1', 'user-b')).rejects.toThrow(\n ForbiddenException\n );\n });\n\n it('allows non-owner with sharing to view configs', async () => {\n prisma.mcpServer.findUnique.mockResolvedValue(server);\n sharingService.isSharedWith.mockResolvedValue(true);\n prisma.mcpServerToolConfig.findMany.mockResolvedValue([]);\n\n const result = await service.getServerToolConfigs('srv-1', 'user-b');\n\n expect(result.tools).toHaveLength(3);\n });\n });\n\n describe('updateServerToolConfigs', () => {\n it('creates config records for all tools in a transaction', async () => {\n prisma.mcpServer.findUnique.mockResolvedValue(server);\n prisma.mcpServerToolConfig.findMany.mockResolvedValue([\n { id: 'tc-1', mcpServerId: 'srv-1', toolName: 'read_file', isEnabled: true },\n { id: 'tc-2', mcpServerId: 'srv-1', toolName: 'write_file', isEnabled: false },\n { id: 'tc-3', mcpServerId: 'srv-1', toolName: 'delete_file', isEnabled: false },\n ]);\n\n await service.updateServerToolConfigs(\n 'srv-1',\n [\n { toolName: 'read_file', isEnabled: true },\n { toolName: 'write_file', isEnabled: false },\n { toolName: 'delete_file', isEnabled: false },\n ],\n 'user-a'\n );\n\n expect(prisma.$transaction).toHaveBeenCalled();\n expect(prisma.mcpServerToolConfig.deleteMany).toHaveBeenCalledWith({\n where: { mcpServerId: 'srv-1' },\n });\n expect(prisma.mcpServerToolConfig.createMany).toHaveBeenCalledWith({\n data: [\n { mcpServerId: 'srv-1', toolName: 'read_file', isEnabled: true },\n { mcpServerId: 'srv-1', toolName: 'write_file', isEnabled: false },\n { mcpServerId: 'srv-1', toolName: 'delete_file', isEnabled: false },\n ],\n });\n });\n\n it('throws ForbiddenException for non-owner', async () => {\n prisma.mcpServer.findUnique.mockResolvedValue(server);\n\n await expect(\n service.updateServerToolConfigs(\n 'srv-1',\n [{ toolName: 'read_file', isEnabled: true }],\n 'user-b'\n )\n ).rejects.toThrow(ForbiddenException);\n });\n\n it('allows admin shared user to update configs', async () => {\n prisma.mcpServer.findUnique.mockResolvedValue(server);\n sharingService.getPermission.mockResolvedValue('admin');\n sharingService.isSharedWith.mockResolvedValue(true);\n prisma.mcpServerToolConfig.findMany.mockResolvedValue([\n { id: 'tc-1', mcpServerId: 'srv-1', toolName: 'read_file', isEnabled: true },\n ]);\n\n const result = await service.updateServerToolConfigs(\n 'srv-1',\n [{ toolName: 'read_file', isEnabled: true }],\n 'user-b'\n );\n\n expect(result.tools).toBeDefined();\n });\n });\n});\n"],"names":["ForbiddenException","beforeEach","describe","expect","it","vi","McpService","McpRegistry","mockInitialize","fn","mockResolvedValue","undefined","mockListTools","name","description","mockShutdown","mock","importOriginal","actual","ExternalMcpServer","MockExternalMcpServer","config","initialize","listTools","shutdown","RemoteHttpMcpServer","MockRemoteHttpMcpServer","RemoteSseMcpServer","MockRemoteSseMcpServer","service","prisma","registry","debugService","sharingService","server","id","type","apiKeyConfig","oauthConfig","userId","profiles","oauthToken","toolsCache","mcpServer","findMany","findUnique","create","update","delete","member","mcpServerToolsCache","mcpServerToolConfig","deleteMany","count","createMany","$transaction","createLog","updateLog","getSharedResourceIds","isSharedWith","getPermission","result","getServerToolConfigs","hasConfigs","toBe","tools","toHaveLength","every","t","isEnabled","mcpServerId","toolName","readFile","find","writeFile","deleteFile","hasConfig","rejects","toThrow","updateServerToolConfigs","toHaveBeenCalled","toHaveBeenCalledWith","where","data","toBeDefined"],"mappings":"AAAA;;CAEC,GAED,SAASA,kBAAkB,QAAQ,iBAAiB;AACpD,SAASC,UAAU,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAS;AAG9D,SAASC,UAAU,QAAQ,mCAAmC;AAC9D,SAASC,WAAW,QAAQ,oCAAoC;AAGhE,gCAAgC;AAChC,MAAMC,iBAAiBH,GAAGI,EAAE,GAAGC,iBAAiB,CAACC;AACjD,MAAMC,gBAAgBP,GAAGI,EAAE,GAAGC,iBAAiB,CAAC;IAC9C;QAAEG,MAAM;QAAaC,aAAa;IAAc;IAChD;QAAED,MAAM;QAAcC,aAAa;IAAe;IAClD;QAAED,MAAM;QAAeC,aAAa;IAAgB;CACrD;AACD,MAAMC,eAAeV,GAAGI,EAAE,GAAGC,iBAAiB,CAACC;AAE/CN,GAAGW,IAAI,CAAC,4BAA4B,OAAOC;IACzC,MAAMC,SAAS,MAAMD;IACrB,OAAO;QACL,GAAGC,MAAM;QACTC,mBAAmB,MAAMC;YAEvB,YAAYC,MAAe,CAAE;qBAG7BC,aAAad;qBACbe,YAAYX;qBACZY,WAAWT;gBAJT,IAAI,CAACM,MAAM,GAAGA;YAChB;QAIF;QACAI,qBAAqB,MAAMC;YAEzB,YAAYL,MAAe,CAAE;qBAG7BC,aAAad;qBACbe,YAAYX;gBAHV,IAAI,CAACS,MAAM,GAAGA;YAChB;QAGF;QACAM,oBAAoB,MAAMC;YAExB,YAAYP,MAAe,CAAE;qBAG7BC,aAAad;qBACbe,YAAYX;gBAHV,IAAI,CAACS,MAAM,GAAGA;YAChB;QAGF;IACF;AACF;AAEAnB,SAAS,6BAA6B;IACpC,IAAI2B;IACJ,+BAA+B;IAC/B,IAAIC;IACJ,IAAIC;IACJ,IAAIC;IACJ,IAAIC;IAEJ,MAAMC,SAAS;QACbC,IAAI;QACJtB,MAAM;QACNuB,MAAM;QACNf,QAAQ;QACRgB,cAAc;QACdC,aAAa;QACbC,QAAQ;QACRC,UAAU,EAAE;QACZC,YAAY;QACZC,YAAY,EAAE;IAChB;IAEAzC,WAAW;QACT6B,SAAS;YACPa,WAAW;gBACTC,UAAUvC,GAAGI,EAAE,GAAGC,iBAAiB,CAAC,EAAE;gBACtCmC,YAAYxC,GAAGI,EAAE,GAAGC,iBAAiB,CAAC;gBACtCoC,QAAQzC,GAAGI,EAAE,GAAGC,iBAAiB,CAAC;oBAAEyB,IAAI;gBAAQ;gBAChDY,QAAQ1C,GAAGI,EAAE,GAAGC,iBAAiB,CAAC,CAAC;gBACnCsC,QAAQ3C,GAAGI,EAAE,GAAGC,iBAAiB,CAACC;YACpC;YACAsC,QAAQ;gBACNL,UAAUvC,GAAGI,EAAE,GAAGC,iBAAiB,CAAC,EAAE;YACxC;YACAwC,qBAAqB;gBACnBN,UAAUvC,GAAGI,EAAE,GAAGC,iBAAiB,CAAC,EAAE;YACxC;YACAyC,qBAAqB;gBACnBP,UAAUvC,GAAGI,EAAE,GAAGC,iBAAiB,CAAC,EAAE;gBACtC0C,YAAY/C,GAAGI,EAAE,GAAGC,iBAAiB,CAAC;oBAAE2C,OAAO;gBAAE;gBACjDC,YAAYjD,GAAGI,EAAE,GAAGC,iBAAiB,CAAC;oBAAE2C,OAAO;gBAAE;YACnD;YACAE,cAAclD,GAAGI,EAAE,CAAC,OAAOA;gBACzB,OAAOA,GAAGqB;YACZ;QACF;QACAC,WAAW,IAAIxB;QACfyB,eAAe;YACbwB,WAAWnD,GAAGI,EAAE,GAAGC,iBAAiB,CAAC;gBAAEyB,IAAI;YAAQ;YACnDsB,WAAWpD,GAAGI,EAAE,GAAGC,iBAAiB,CAAC,CAAC;QACxC;QACAuB,iBAAiB;YACfyB,sBAAsBrD,GAAGI,EAAE,GAAGC,iBAAiB,CAAC,EAAE;YAClDiD,cAActD,GAAGI,EAAE,GAAGC,iBAAiB,CAAC;YACxCkD,eAAevD,GAAGI,EAAE,GAAGC,iBAAiB,CAAC;QAC3C;QACAmB,UAAU,IAAIvB,WACZwB,QACAC,UACAC,cACAC;IAEJ;IAEA/B,SAAS,wBAAwB;QAC/BE,GAAG,4EAA4E;YAC7E0B,OAAOa,SAAS,CAACE,UAAU,CAACnC,iBAAiB,CAACwB;YAC9CJ,OAAOqB,mBAAmB,CAACP,QAAQ,CAAClC,iBAAiB,CAAC,EAAE;YAExD,MAAMmD,SAAS,MAAMhC,QAAQiC,oBAAoB,CAAC,SAAS;YAE3D3D,OAAO0D,OAAOE,UAAU,EAAEC,IAAI,CAAC;YAC/B7D,OAAO0D,OAAOI,KAAK,EAAEC,YAAY,CAAC;YAClC/D,OAAO0D,OAAOI,KAAK,CAACE,KAAK,CAAC,CAACC,IAA8BA,EAAEC,SAAS,GAAGL,IAAI,CAAC;QAC9E;QAEA5D,GAAG,iEAAiE;YAClE0B,OAAOa,SAAS,CAACE,UAAU,CAACnC,iBAAiB,CAACwB;YAC9CJ,OAAOqB,mBAAmB,CAACP,QAAQ,CAAClC,iBAAiB,CAAC;gBACpD;oBAAEyB,IAAI;oBAAQmC,aAAa;oBAASC,UAAU;oBAAaF,WAAW;gBAAK;gBAC3E;oBAAElC,IAAI;oBAAQmC,aAAa;oBAASC,UAAU;oBAAcF,WAAW;gBAAM;gBAC7E;oBAAElC,IAAI;oBAAQmC,aAAa;oBAASC,UAAU;oBAAeF,WAAW;gBAAM;aAC/E;YAED,MAAMR,SAAS,MAAMhC,QAAQiC,oBAAoB,CAAC,SAAS;YAE3D3D,OAAO0D,OAAOE,UAAU,EAAEC,IAAI,CAAC;YAC/B7D,OAAO0D,OAAOI,KAAK,EAAEC,YAAY,CAAC;YAElC,MAAMM,WAAWX,OAAOI,KAAK,CAACQ,IAAI,CAAC,CAACL,IAAwBA,EAAEvD,IAAI,KAAK;YACvE,MAAM6D,YAAYb,OAAOI,KAAK,CAACQ,IAAI,CAAC,CAACL,IAAwBA,EAAEvD,IAAI,KAAK;YACxE,MAAM8D,aAAad,OAAOI,KAAK,CAACQ,IAAI,CAAC,CAACL,IAAwBA,EAAEvD,IAAI,KAAK;YAEzEV,OAAOqE,UAAUH,WAAWL,IAAI,CAAC;YACjC7D,OAAOuE,WAAWL,WAAWL,IAAI,CAAC;YAClC7D,OAAOwE,YAAYN,WAAWL,IAAI,CAAC;QACrC;QAEA5D,GAAG,sEAAsE;YACvE0B,OAAOa,SAAS,CAACE,UAAU,CAACnC,iBAAiB,CAACwB;YAC9C,qEAAqE;YACrEJ,OAAOqB,mBAAmB,CAACP,QAAQ,CAAClC,iBAAiB,CAAC;gBACpD;oBAAEyB,IAAI;oBAAQmC,aAAa;oBAASC,UAAU;oBAAaF,WAAW;gBAAK;aAC5E;YAED,MAAMR,SAAS,MAAMhC,QAAQiC,oBAAoB,CAAC,SAAS;YAE3D3D,OAAO0D,OAAOE,UAAU,EAAEC,IAAI,CAAC;YAE/B,MAAMQ,WAAWX,OAAOI,KAAK,CAACQ,IAAI,CAAC,CAACL,IAAwBA,EAAEvD,IAAI,KAAK;YACvE,MAAM6D,YAAYb,OAAOI,KAAK,CAACQ,IAAI,CAAC,CAACL,IAAwBA,EAAEvD,IAAI,KAAK;YACxE,MAAM8D,aAAad,OAAOI,KAAK,CAACQ,IAAI,CAAC,CAACL,IAAwBA,EAAEvD,IAAI,KAAK;YAEzEV,OAAOqE,UAAUH,WAAWL,IAAI,CAAC;YACjC7D,OAAOqE,UAAUI,WAAWZ,IAAI,CAAC;YACjC7D,OAAOuE,WAAWL,WAAWL,IAAI,CAAC;YAClC7D,OAAOuE,WAAWE,WAAWZ,IAAI,CAAC;YAClC7D,OAAOwE,YAAYN,WAAWL,IAAI,CAAC;YACnC7D,OAAOwE,YAAYC,WAAWZ,IAAI,CAAC;QACrC;QAEA5D,GAAG,2DAA2D;YAC5D0B,OAAOa,SAAS,CAACE,UAAU,CAACnC,iBAAiB,CAACwB;YAE9C,MAAM/B,OAAO0B,QAAQiC,oBAAoB,CAAC,SAAS,WAAWe,OAAO,CAACC,OAAO,CAC3E9E;QAEJ;QAEAI,GAAG,iDAAiD;YAClD0B,OAAOa,SAAS,CAACE,UAAU,CAACnC,iBAAiB,CAACwB;YAC9CD,eAAe0B,YAAY,CAACjD,iBAAiB,CAAC;YAC9CoB,OAAOqB,mBAAmB,CAACP,QAAQ,CAAClC,iBAAiB,CAAC,EAAE;YAExD,MAAMmD,SAAS,MAAMhC,QAAQiC,oBAAoB,CAAC,SAAS;YAE3D3D,OAAO0D,OAAOI,KAAK,EAAEC,YAAY,CAAC;QACpC;IACF;IAEAhE,SAAS,2BAA2B;QAClCE,GAAG,yDAAyD;YAC1D0B,OAAOa,SAAS,CAACE,UAAU,CAACnC,iBAAiB,CAACwB;YAC9CJ,OAAOqB,mBAAmB,CAACP,QAAQ,CAAClC,iBAAiB,CAAC;gBACpD;oBAAEyB,IAAI;oBAAQmC,aAAa;oBAASC,UAAU;oBAAaF,WAAW;gBAAK;gBAC3E;oBAAElC,IAAI;oBAAQmC,aAAa;oBAASC,UAAU;oBAAcF,WAAW;gBAAM;gBAC7E;oBAAElC,IAAI;oBAAQmC,aAAa;oBAASC,UAAU;oBAAeF,WAAW;gBAAM;aAC/E;YAED,MAAMxC,QAAQkD,uBAAuB,CACnC,SACA;gBACE;oBAAER,UAAU;oBAAaF,WAAW;gBAAK;gBACzC;oBAAEE,UAAU;oBAAcF,WAAW;gBAAM;gBAC3C;oBAAEE,UAAU;oBAAeF,WAAW;gBAAM;aAC7C,EACD;YAGFlE,OAAO2B,OAAOyB,YAAY,EAAEyB,gBAAgB;YAC5C7E,OAAO2B,OAAOqB,mBAAmB,CAACC,UAAU,EAAE6B,oBAAoB,CAAC;gBACjEC,OAAO;oBAAEZ,aAAa;gBAAQ;YAChC;YACAnE,OAAO2B,OAAOqB,mBAAmB,CAACG,UAAU,EAAE2B,oBAAoB,CAAC;gBACjEE,MAAM;oBACJ;wBAAEb,aAAa;wBAASC,UAAU;wBAAaF,WAAW;oBAAK;oBAC/D;wBAAEC,aAAa;wBAASC,UAAU;wBAAcF,WAAW;oBAAM;oBACjE;wBAAEC,aAAa;wBAASC,UAAU;wBAAeF,WAAW;oBAAM;iBACnE;YACH;QACF;QAEAjE,GAAG,2CAA2C;YAC5C0B,OAAOa,SAAS,CAACE,UAAU,CAACnC,iBAAiB,CAACwB;YAE9C,MAAM/B,OACJ0B,QAAQkD,uBAAuB,CAC7B,SACA;gBAAC;oBAAER,UAAU;oBAAaF,WAAW;gBAAK;aAAE,EAC5C,WAEFQ,OAAO,CAACC,OAAO,CAAC9E;QACpB;QAEAI,GAAG,8CAA8C;YAC/C0B,OAAOa,SAAS,CAACE,UAAU,CAACnC,iBAAiB,CAACwB;YAC9CD,eAAe2B,aAAa,CAAClD,iBAAiB,CAAC;YAC/CuB,eAAe0B,YAAY,CAACjD,iBAAiB,CAAC;YAC9CoB,OAAOqB,mBAAmB,CAACP,QAAQ,CAAClC,iBAAiB,CAAC;gBACpD;oBAAEyB,IAAI;oBAAQmC,aAAa;oBAASC,UAAU;oBAAaF,WAAW;gBAAK;aAC5E;YAED,MAAMR,SAAS,MAAMhC,QAAQkD,uBAAuB,CAClD,SACA;gBAAC;oBAAER,UAAU;oBAAaF,WAAW;gBAAK;aAAE,EAC5C;YAGFlE,OAAO0D,OAAOI,KAAK,EAAEmB,WAAW;QAClC;IACF;AACF"}
|