@forestadmin/mcp-server 0.1.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/README.md +128 -0
- package/dist/__mocks__/version.d.ts +3 -0
- package/dist/__mocks__/version.js +7 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +14 -0
- package/dist/factory.d.ts +51 -0
- package/dist/factory.js +40 -0
- package/dist/forest-oauth-provider.d.ts +44 -0
- package/dist/forest-oauth-provider.js +253 -0
- package/dist/forest-oauth-provider.test.d.ts +2 -0
- package/dist/forest-oauth-provider.test.js +590 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +13 -0
- package/dist/mcp-paths.d.ts +5 -0
- package/dist/mcp-paths.js +11 -0
- package/dist/polyfills.d.ts +12 -0
- package/dist/polyfills.js +27 -0
- package/dist/schemas/filter.d.ts +4 -0
- package/dist/schemas/filter.js +70 -0
- package/dist/schemas/filter.test.d.ts +2 -0
- package/dist/schemas/filter.test.js +234 -0
- package/dist/server.d.ts +87 -0
- package/dist/server.js +341 -0
- package/dist/server.test.d.ts +2 -0
- package/dist/server.test.js +901 -0
- package/dist/test-utils/mock-server.d.ts +62 -0
- package/dist/test-utils/mock-server.js +187 -0
- package/dist/tools/list.d.ts +4 -0
- package/dist/tools/list.js +98 -0
- package/dist/tools/list.test.d.ts +2 -0
- package/dist/tools/list.test.js +385 -0
- package/dist/utils/activity-logs-creator.d.ts +9 -0
- package/dist/utils/activity-logs-creator.js +65 -0
- package/dist/utils/activity-logs-creator.test.d.ts +2 -0
- package/dist/utils/activity-logs-creator.test.js +239 -0
- package/dist/utils/agent-caller.d.ts +13 -0
- package/dist/utils/agent-caller.js +24 -0
- package/dist/utils/agent-caller.test.d.ts +2 -0
- package/dist/utils/agent-caller.test.js +102 -0
- package/dist/utils/error-parser.d.ts +10 -0
- package/dist/utils/error-parser.js +56 -0
- package/dist/utils/error-parser.test.d.ts +2 -0
- package/dist/utils/error-parser.test.js +124 -0
- package/dist/utils/schema-fetcher.d.ts +53 -0
- package/dist/utils/schema-fetcher.js +85 -0
- package/dist/utils/schema-fetcher.test.d.ts +2 -0
- package/dist/utils/schema-fetcher.test.js +212 -0
- package/dist/utils/sse-error-logger.d.ts +14 -0
- package/dist/utils/sse-error-logger.js +112 -0
- package/dist/utils/tool-with-logging.d.ts +44 -0
- package/dist/utils/tool-with-logging.js +66 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +43 -0
- package/package.json +49 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
7
|
+
const supertest_1 = __importDefault(require("supertest"));
|
|
8
|
+
const server_1 = __importDefault(require("./server"));
|
|
9
|
+
const mock_server_1 = __importDefault(require("./test-utils/mock-server"));
|
|
10
|
+
function shutDownHttpServer(server) {
|
|
11
|
+
if (!server)
|
|
12
|
+
return Promise.resolve();
|
|
13
|
+
return new Promise(resolve => {
|
|
14
|
+
server.close(() => {
|
|
15
|
+
resolve();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Integration tests for ForestMCPServer instance
|
|
21
|
+
* Tests the actual server class and its behavior
|
|
22
|
+
*/
|
|
23
|
+
describe('ForestMCPServer Instance', () => {
|
|
24
|
+
let server;
|
|
25
|
+
let originalEnv;
|
|
26
|
+
let modifiedEnv;
|
|
27
|
+
let mockServer;
|
|
28
|
+
const originalFetch = global.fetch;
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
originalEnv = { ...process.env };
|
|
31
|
+
process.env.FOREST_ENV_SECRET = 'test-env-secret';
|
|
32
|
+
process.env.FOREST_AUTH_SECRET = 'test-auth-secret';
|
|
33
|
+
process.env.FOREST_SERVER_URL = 'https://test.forestadmin.com';
|
|
34
|
+
process.env.AGENT_HOSTNAME = 'http://localhost:3310';
|
|
35
|
+
// Setup mock for Forest Admin server
|
|
36
|
+
mockServer = new mock_server_1.default();
|
|
37
|
+
mockServer
|
|
38
|
+
.get('/liana/environment', {
|
|
39
|
+
data: { id: '12345', attributes: { api_endpoint: 'https://api.example.com' } },
|
|
40
|
+
})
|
|
41
|
+
.get('/liana/forest-schema', {
|
|
42
|
+
data: [
|
|
43
|
+
{
|
|
44
|
+
id: 'users',
|
|
45
|
+
type: 'collections',
|
|
46
|
+
attributes: { name: 'users', fields: [{ field: 'id', type: 'Number' }] },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'products',
|
|
50
|
+
type: 'collections',
|
|
51
|
+
attributes: { name: 'products', fields: [{ field: 'name', type: 'String' }] },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
meta: { liana: 'forest-express-sequelize', liana_version: '9.0.0', liana_features: null },
|
|
55
|
+
})
|
|
56
|
+
.get(/\/oauth\/register\/registered-client/, {
|
|
57
|
+
client_id: 'registered-client',
|
|
58
|
+
redirect_uris: ['https://example.com/callback'],
|
|
59
|
+
client_name: 'Test Client',
|
|
60
|
+
})
|
|
61
|
+
.get(/\/oauth\/register\//, { error: 'Client not found' }, 404);
|
|
62
|
+
global.fetch = mockServer.fetch;
|
|
63
|
+
});
|
|
64
|
+
afterAll(async () => {
|
|
65
|
+
process.env = originalEnv;
|
|
66
|
+
global.fetch = originalFetch;
|
|
67
|
+
});
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
modifiedEnv = { ...process.env };
|
|
70
|
+
mockServer.clear();
|
|
71
|
+
});
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
process.env = modifiedEnv;
|
|
74
|
+
});
|
|
75
|
+
describe('constructor', () => {
|
|
76
|
+
it('should create server instance', () => {
|
|
77
|
+
server = new server_1.default();
|
|
78
|
+
expect(server).toBeDefined();
|
|
79
|
+
expect(server).toBeInstanceOf(server_1.default);
|
|
80
|
+
});
|
|
81
|
+
it('should initialize with FOREST_SERVER_URL', () => {
|
|
82
|
+
process.env.FOREST_SERVER_URL = 'https://custom.forestadmin.com';
|
|
83
|
+
server = new server_1.default();
|
|
84
|
+
expect(server.forestServerUrl).toBe('https://custom.forestadmin.com');
|
|
85
|
+
});
|
|
86
|
+
it('should fallback to FOREST_URL', () => {
|
|
87
|
+
delete process.env.FOREST_SERVER_URL;
|
|
88
|
+
process.env.FOREST_URL = 'https://fallback.forestadmin.com';
|
|
89
|
+
server = new server_1.default();
|
|
90
|
+
expect(server.forestServerUrl).toBe('https://fallback.forestadmin.com');
|
|
91
|
+
});
|
|
92
|
+
it('should use default URL when neither is provided', () => {
|
|
93
|
+
delete process.env.FOREST_SERVER_URL;
|
|
94
|
+
delete process.env.FOREST_URL;
|
|
95
|
+
server = new server_1.default();
|
|
96
|
+
expect(server.forestServerUrl).toBe('https://api.forestadmin.com');
|
|
97
|
+
});
|
|
98
|
+
it('should create MCP server instance', () => {
|
|
99
|
+
server = new server_1.default();
|
|
100
|
+
expect(server.mcpServer).toBeDefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('environment validation', () => {
|
|
104
|
+
it('should throw error when FOREST_ENV_SECRET is missing', async () => {
|
|
105
|
+
delete process.env.FOREST_ENV_SECRET;
|
|
106
|
+
server = new server_1.default();
|
|
107
|
+
await expect(server.run()).rejects.toThrow('FOREST_ENV_SECRET is not set. Provide it via options.envSecret or FOREST_ENV_SECRET environment variable.');
|
|
108
|
+
});
|
|
109
|
+
it('should throw error when FOREST_AUTH_SECRET is missing', async () => {
|
|
110
|
+
delete process.env.FOREST_AUTH_SECRET;
|
|
111
|
+
server = new server_1.default();
|
|
112
|
+
await expect(server.run()).rejects.toThrow('FOREST_AUTH_SECRET is not set. Provide it via options.authSecret or FOREST_AUTH_SECRET environment variable.');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('run method', () => {
|
|
116
|
+
afterEach(async () => {
|
|
117
|
+
await shutDownHttpServer(server?.httpServer);
|
|
118
|
+
});
|
|
119
|
+
it('should start server on specified port', async () => {
|
|
120
|
+
const testPort = 39310; // Use a different port for testing
|
|
121
|
+
process.env.MCP_SERVER_PORT = testPort.toString();
|
|
122
|
+
server = new server_1.default();
|
|
123
|
+
// Start the server without awaiting (it runs indefinitely)
|
|
124
|
+
server.run();
|
|
125
|
+
// Wait a bit for the server to start
|
|
126
|
+
await new Promise(resolve => {
|
|
127
|
+
setTimeout(resolve, 500);
|
|
128
|
+
});
|
|
129
|
+
// Verify the server is running by making a request
|
|
130
|
+
const { httpServer } = server;
|
|
131
|
+
expect(httpServer).toBeDefined();
|
|
132
|
+
// Make a request to verify server is responding
|
|
133
|
+
const response = await (0, supertest_1.default)(httpServer)
|
|
134
|
+
.post('/mcp')
|
|
135
|
+
.send({ jsonrpc: '2.0', method: 'tools/list', id: 1 });
|
|
136
|
+
expect(response.status).toBeDefined();
|
|
137
|
+
});
|
|
138
|
+
it('should create transport instance', async () => {
|
|
139
|
+
const testPort = 39311;
|
|
140
|
+
process.env.MCP_SERVER_PORT = testPort.toString();
|
|
141
|
+
server = new server_1.default();
|
|
142
|
+
server.run();
|
|
143
|
+
await new Promise(resolve => {
|
|
144
|
+
setTimeout(resolve, 500);
|
|
145
|
+
});
|
|
146
|
+
expect(server.mcpTransport).toBeDefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('HTTP endpoint', () => {
|
|
150
|
+
let httpServer;
|
|
151
|
+
beforeAll(async () => {
|
|
152
|
+
const testPort = 39312;
|
|
153
|
+
process.env.MCP_SERVER_PORT = testPort.toString();
|
|
154
|
+
server = new server_1.default();
|
|
155
|
+
server.run();
|
|
156
|
+
await new Promise(resolve => {
|
|
157
|
+
setTimeout(resolve, 500);
|
|
158
|
+
});
|
|
159
|
+
httpServer = server.httpServer;
|
|
160
|
+
});
|
|
161
|
+
afterAll(async () => {
|
|
162
|
+
await shutDownHttpServer(server?.httpServer);
|
|
163
|
+
});
|
|
164
|
+
it('should handle POST requests to /mcp', async () => {
|
|
165
|
+
const response = await (0, supertest_1.default)(httpServer)
|
|
166
|
+
.post('/mcp')
|
|
167
|
+
.send({ jsonrpc: '2.0', method: 'initialize', id: 1 });
|
|
168
|
+
expect(response.status).not.toBe(404);
|
|
169
|
+
});
|
|
170
|
+
it('should reject GET requests', async () => {
|
|
171
|
+
const response = await (0, supertest_1.default)(httpServer).get('/mcp');
|
|
172
|
+
expect(response.status).toBe(405);
|
|
173
|
+
});
|
|
174
|
+
it('should handle CORS', async () => {
|
|
175
|
+
const response = await (0, supertest_1.default)(httpServer)
|
|
176
|
+
.post('/mcp')
|
|
177
|
+
.set('Origin', 'https://example.com')
|
|
178
|
+
.send({ jsonrpc: '2.0', method: 'initialize', id: 1 });
|
|
179
|
+
expect(response.headers['access-control-allow-origin']).toBe('*');
|
|
180
|
+
});
|
|
181
|
+
it('should return JSON-RPC error on transport failure', async () => {
|
|
182
|
+
// Send invalid request
|
|
183
|
+
const response = await (0, supertest_1.default)(httpServer).post('/mcp').send('invalid json');
|
|
184
|
+
// Should handle the error gracefully
|
|
185
|
+
expect(response.status).toBeGreaterThanOrEqual(400);
|
|
186
|
+
});
|
|
187
|
+
describe('OAuth metadata endpoint', () => {
|
|
188
|
+
it('should return OAuth metadata at /.well-known/oauth-authorization-server', async () => {
|
|
189
|
+
const response = await (0, supertest_1.default)(server.httpServer).get('/.well-known/oauth-authorization-server');
|
|
190
|
+
expect(response.status).toBe(200);
|
|
191
|
+
expect(response.headers['content-type']).toMatch(/application\/json/);
|
|
192
|
+
expect(response.body.issuer).toBe('http://localhost:39312/');
|
|
193
|
+
expect(response.body.registration_endpoint).toBe('https://test.forestadmin.com/oauth/register');
|
|
194
|
+
expect(response.body.authorization_endpoint).toBe(`http://localhost:39312/oauth/authorize`);
|
|
195
|
+
expect(response.body.token_endpoint).toBe(`http://localhost:39312/oauth/token`);
|
|
196
|
+
expect(response.body.revocation_endpoint).toBeUndefined();
|
|
197
|
+
expect(response.body.scopes_supported).toEqual([
|
|
198
|
+
'mcp:read',
|
|
199
|
+
'mcp:write',
|
|
200
|
+
'mcp:action',
|
|
201
|
+
'mcp:admin',
|
|
202
|
+
]);
|
|
203
|
+
expect(response.body.response_types_supported).toEqual(['code']);
|
|
204
|
+
expect(response.body.grant_types_supported).toEqual([
|
|
205
|
+
'authorization_code',
|
|
206
|
+
'refresh_token',
|
|
207
|
+
]);
|
|
208
|
+
expect(response.body.code_challenge_methods_supported).toEqual(['S256']);
|
|
209
|
+
expect(response.body.token_endpoint_auth_methods_supported).toEqual(['none']);
|
|
210
|
+
});
|
|
211
|
+
it('should return registration_endpoint with custom FOREST_SERVER_URL', async () => {
|
|
212
|
+
// Clean up previous server
|
|
213
|
+
await shutDownHttpServer(server?.httpServer);
|
|
214
|
+
process.env.FOREST_SERVER_URL = 'https://custom.forestadmin.com';
|
|
215
|
+
process.env.MCP_SERVER_PORT = '39314';
|
|
216
|
+
server = new server_1.default();
|
|
217
|
+
server.run();
|
|
218
|
+
await new Promise(resolve => {
|
|
219
|
+
setTimeout(resolve, 500);
|
|
220
|
+
});
|
|
221
|
+
const response = await (0, supertest_1.default)(server.httpServer).get('/.well-known/oauth-authorization-server');
|
|
222
|
+
expect(response.body.registration_endpoint).toBe('https://custom.forestadmin.com/oauth/register');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('/oauth/authorize endpoint', () => {
|
|
226
|
+
it('should return 400 when required parameters are missing', async () => {
|
|
227
|
+
const response = await (0, supertest_1.default)(httpServer).get('/oauth/authorize');
|
|
228
|
+
expect(response.status).toBe(400);
|
|
229
|
+
});
|
|
230
|
+
it('should return 400 when client_id is missing', async () => {
|
|
231
|
+
const response = await (0, supertest_1.default)(httpServer).get('/oauth/authorize').query({
|
|
232
|
+
redirect_uri: 'https://example.com/callback',
|
|
233
|
+
response_type: 'code',
|
|
234
|
+
code_challenge: 'test-challenge',
|
|
235
|
+
code_challenge_method: 'S256',
|
|
236
|
+
state: 'test-state',
|
|
237
|
+
});
|
|
238
|
+
expect(response.status).toBe(400);
|
|
239
|
+
});
|
|
240
|
+
it('should return 400 when redirect_uri is missing', async () => {
|
|
241
|
+
const response = await (0, supertest_1.default)(httpServer).get('/oauth/authorize').query({
|
|
242
|
+
client_id: 'test-client',
|
|
243
|
+
response_type: 'code',
|
|
244
|
+
code_challenge: 'test-challenge',
|
|
245
|
+
code_challenge_method: 'S256',
|
|
246
|
+
state: 'test-state',
|
|
247
|
+
});
|
|
248
|
+
expect(response.status).toBe(400);
|
|
249
|
+
});
|
|
250
|
+
it('should return 400 when code_challenge is missing', async () => {
|
|
251
|
+
const response = await (0, supertest_1.default)(httpServer).get('/oauth/authorize').query({
|
|
252
|
+
client_id: 'test-client',
|
|
253
|
+
redirect_uri: 'https://example.com/callback',
|
|
254
|
+
response_type: 'code',
|
|
255
|
+
code_challenge_method: 'S256',
|
|
256
|
+
state: 'test-state',
|
|
257
|
+
});
|
|
258
|
+
expect(response.status).toBe(400);
|
|
259
|
+
});
|
|
260
|
+
it('should return 400 when client is not registered', async () => {
|
|
261
|
+
const response = await (0, supertest_1.default)(httpServer).get('/oauth/authorize').query({
|
|
262
|
+
client_id: 'unregistered-client',
|
|
263
|
+
redirect_uri: 'https://example.com/callback',
|
|
264
|
+
response_type: 'code',
|
|
265
|
+
code_challenge: 'test-challenge',
|
|
266
|
+
code_challenge_method: 'S256',
|
|
267
|
+
state: 'test-state',
|
|
268
|
+
scope: 'mcp:read',
|
|
269
|
+
});
|
|
270
|
+
expect(response.status).toBe(400);
|
|
271
|
+
});
|
|
272
|
+
it('should redirect to Forest Admin frontend with correct parameters', async () => {
|
|
273
|
+
const response = await (0, supertest_1.default)(httpServer).get('/oauth/authorize').query({
|
|
274
|
+
client_id: 'registered-client',
|
|
275
|
+
redirect_uri: 'https://example.com/callback',
|
|
276
|
+
response_type: 'code',
|
|
277
|
+
code_challenge: 'test-challenge',
|
|
278
|
+
code_challenge_method: 'S256',
|
|
279
|
+
state: 'test-state',
|
|
280
|
+
scope: 'mcp:read profile',
|
|
281
|
+
});
|
|
282
|
+
expect(response.status).toBe(302);
|
|
283
|
+
expect(response.headers.location).toContain('https://app.forestadmin.com/oauth/authorize');
|
|
284
|
+
const redirectUrl = new URL(response.headers.location);
|
|
285
|
+
expect(redirectUrl.searchParams.get('redirect_uri')).toBe('https://example.com/callback');
|
|
286
|
+
expect(redirectUrl.searchParams.get('code_challenge')).toBe('test-challenge');
|
|
287
|
+
expect(redirectUrl.searchParams.get('code_challenge_method')).toBe('S256');
|
|
288
|
+
expect(redirectUrl.searchParams.get('response_type')).toBe('code');
|
|
289
|
+
expect(redirectUrl.searchParams.get('client_id')).toBe('registered-client');
|
|
290
|
+
expect(redirectUrl.searchParams.get('state')).toBe('test-state');
|
|
291
|
+
expect(redirectUrl.searchParams.get('scope')).toBe('mcp:read+profile');
|
|
292
|
+
expect(redirectUrl.searchParams.get('environmentId')).toBe('12345');
|
|
293
|
+
});
|
|
294
|
+
it('should redirect to default frontend when FOREST_FRONTEND_HOSTNAME is not set', async () => {
|
|
295
|
+
const response = await (0, supertest_1.default)(httpServer).get('/oauth/authorize').query({
|
|
296
|
+
client_id: 'registered-client',
|
|
297
|
+
redirect_uri: 'https://example.com/callback',
|
|
298
|
+
response_type: 'code',
|
|
299
|
+
code_challenge: 'test-challenge',
|
|
300
|
+
code_challenge_method: 'S256',
|
|
301
|
+
state: 'test-state',
|
|
302
|
+
scope: 'mcp:read',
|
|
303
|
+
});
|
|
304
|
+
expect(response.status).toBe(302);
|
|
305
|
+
expect(response.headers.location).toContain('https://app.forestadmin.com/oauth/authorize');
|
|
306
|
+
});
|
|
307
|
+
it('should handle POST method for authorize', async () => {
|
|
308
|
+
// POST /authorize uses form-encoded body
|
|
309
|
+
const response = await (0, supertest_1.default)(httpServer).post('/oauth/authorize').type('form').send({
|
|
310
|
+
client_id: 'registered-client',
|
|
311
|
+
redirect_uri: 'https://example.com/callback',
|
|
312
|
+
response_type: 'code',
|
|
313
|
+
code_challenge: 'test-challenge',
|
|
314
|
+
code_challenge_method: 'S256',
|
|
315
|
+
state: 'test-state',
|
|
316
|
+
scope: 'mcp:read',
|
|
317
|
+
resource: 'https://example.com/resource',
|
|
318
|
+
});
|
|
319
|
+
expect(response.status).toBe(302);
|
|
320
|
+
expect(response.headers.location).toStrictEqual(`https://app.forestadmin.com/oauth/authorize?redirect_uri=${encodeURIComponent('https://example.com/callback')}&code_challenge=test-challenge&code_challenge_method=S256&response_type=code&client_id=registered-client&state=test-state&scope=${encodeURIComponent('mcp:read')}&resource=${encodeURIComponent('https://example.com/resource')}&environmentId=12345`);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
/**
|
|
325
|
+
* Integration tests for /oauth/token endpoint
|
|
326
|
+
* Uses a separate server instance with mock server for Forest Admin API
|
|
327
|
+
*/
|
|
328
|
+
describe('/oauth/token endpoint', () => {
|
|
329
|
+
let mcpServer;
|
|
330
|
+
let mcpHttpServer;
|
|
331
|
+
let mcpMockServer;
|
|
332
|
+
beforeAll(async () => {
|
|
333
|
+
process.env.FOREST_ENV_SECRET = 'test-env-secret';
|
|
334
|
+
process.env.FOREST_AUTH_SECRET = 'test-auth-secret';
|
|
335
|
+
process.env.FOREST_SERVER_URL = 'https://test.forestadmin.com';
|
|
336
|
+
process.env.MCP_SERVER_PORT = '39320';
|
|
337
|
+
// Setup mock for Forest Admin server API responses
|
|
338
|
+
mcpMockServer = new mock_server_1.default();
|
|
339
|
+
mcpMockServer
|
|
340
|
+
.get('/liana/environment', {
|
|
341
|
+
data: { id: '12345', attributes: { api_endpoint: 'https://api.example.com' } },
|
|
342
|
+
})
|
|
343
|
+
.get('/liana/forest-schema', {
|
|
344
|
+
data: [
|
|
345
|
+
{
|
|
346
|
+
id: 'users',
|
|
347
|
+
type: 'collections',
|
|
348
|
+
attributes: { name: 'users', fields: [{ field: 'id', type: 'Number' }] },
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: 'products',
|
|
352
|
+
type: 'collections',
|
|
353
|
+
attributes: { name: 'products', fields: [{ field: 'name', type: 'String' }] },
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
meta: { liana: 'forest-express-sequelize', liana_version: '9.0.0', liana_features: null },
|
|
357
|
+
})
|
|
358
|
+
.get(/\/oauth\/register\/registered-client/, {
|
|
359
|
+
client_id: 'registered-client',
|
|
360
|
+
redirect_uris: ['https://example.com/callback'],
|
|
361
|
+
client_name: 'Test Client',
|
|
362
|
+
scope: 'mcp:read mcp:write',
|
|
363
|
+
})
|
|
364
|
+
.get(/\/oauth\/register\//, { error: 'Client not found' }, 404)
|
|
365
|
+
// Mock Forest Admin OAuth token endpoint - returns valid JWTs with meta.renderingId, exp, iat, scope
|
|
366
|
+
// access_token JWT payload: { meta: { renderingId: 456 }, scope: 'mcp:read mcp:write', iat: 2524608000, exp: 2524611600 }
|
|
367
|
+
// refresh_token JWT payload: { iat: 2524608000, exp: 2525212800 }
|
|
368
|
+
.post('/oauth/token', {
|
|
369
|
+
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXRhIjp7InJlbmRlcmluZ0lkIjo0NTZ9LCJzY29wZSI6Im1jcDpyZWFkIG1jcDp3cml0ZSIsImlhdCI6MjUyNDYwODAwMCwiZXhwIjoyNTI0NjExNjAwfQ.fake',
|
|
370
|
+
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjI1MjQ2MDgwMDAsImV4cCI6MjUyNTIxMjgwMH0.fake',
|
|
371
|
+
expires_in: 3600,
|
|
372
|
+
token_type: 'Bearer',
|
|
373
|
+
scope: 'mcp:read mcp:write',
|
|
374
|
+
})
|
|
375
|
+
// Mock Forest Admin user info endpoint (called by forestadmin-client via superagent)
|
|
376
|
+
.get(/\/liana\/v2\/renderings\/\d+\/authorization/, {
|
|
377
|
+
data: {
|
|
378
|
+
id: '123',
|
|
379
|
+
attributes: {
|
|
380
|
+
email: 'user@example.com',
|
|
381
|
+
first_name: 'Test',
|
|
382
|
+
last_name: 'User',
|
|
383
|
+
teams: ['Operations'],
|
|
384
|
+
role: 'Admin',
|
|
385
|
+
permission_level: 'admin',
|
|
386
|
+
tags: [],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
global.fetch = mcpMockServer.fetch;
|
|
391
|
+
// Also mock superagent for forestadmin-client requests
|
|
392
|
+
mcpMockServer.setupSuperagentMock();
|
|
393
|
+
// Create and start server
|
|
394
|
+
mcpServer = new server_1.default();
|
|
395
|
+
mcpServer.run();
|
|
396
|
+
await new Promise(resolve => {
|
|
397
|
+
setTimeout(resolve, 500);
|
|
398
|
+
});
|
|
399
|
+
mcpHttpServer = mcpServer.httpServer;
|
|
400
|
+
});
|
|
401
|
+
afterAll(async () => {
|
|
402
|
+
mcpMockServer.restoreSuperagent();
|
|
403
|
+
await new Promise(resolve => {
|
|
404
|
+
if (mcpServer?.httpServer) {
|
|
405
|
+
mcpServer.httpServer.close(() => resolve());
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
resolve();
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
it('should return 400 when grant_type is missing', async () => {
|
|
413
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
414
|
+
code: 'auth-code-123',
|
|
415
|
+
redirect_uri: 'https://example.com/callback',
|
|
416
|
+
client_id: 'registered-client',
|
|
417
|
+
});
|
|
418
|
+
expect(response.status).toBe(400);
|
|
419
|
+
expect(response.body.error).toBe('invalid_request');
|
|
420
|
+
});
|
|
421
|
+
it('should return 400 when code is missing', async () => {
|
|
422
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
423
|
+
grant_type: 'authorization_code',
|
|
424
|
+
redirect_uri: 'https://example.com/callback',
|
|
425
|
+
client_id: 'registered-client',
|
|
426
|
+
});
|
|
427
|
+
expect(response.status).toBe(400);
|
|
428
|
+
expect(response.body.error).toBe('invalid_request');
|
|
429
|
+
});
|
|
430
|
+
it('should call Forest Admin server to exchange code', async () => {
|
|
431
|
+
mcpMockServer.clear();
|
|
432
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
433
|
+
grant_type: 'authorization_code',
|
|
434
|
+
code: 'valid-auth-code',
|
|
435
|
+
redirect_uri: 'https://example.com/callback',
|
|
436
|
+
client_id: 'registered-client',
|
|
437
|
+
code_verifier: 'test-code-verifier',
|
|
438
|
+
});
|
|
439
|
+
expect(mcpMockServer.fetch).toHaveBeenCalledWith('https://test.forestadmin.com/oauth/token', expect.objectContaining({
|
|
440
|
+
method: 'POST',
|
|
441
|
+
body: expect.stringContaining('"grant_type":"authorization_code"'),
|
|
442
|
+
}));
|
|
443
|
+
expect(response.status).toBe(200);
|
|
444
|
+
expect(response.body.access_token).toBeDefined();
|
|
445
|
+
expect(response.body.refresh_token).toBeDefined();
|
|
446
|
+
expect(response.body.token_type).toBe('Bearer');
|
|
447
|
+
// expires_in is calculated as exp - now from the JWT, so it's a large value for our test tokens
|
|
448
|
+
expect(response.body.expires_in).toBeGreaterThan(0);
|
|
449
|
+
// The scope is returned from the decoded forest token
|
|
450
|
+
expect(response.body.scope).toBe('mcp:read mcp:write');
|
|
451
|
+
const accessToken = response.body.access_token;
|
|
452
|
+
expect(() => jsonwebtoken_1.default.verify(accessToken, process.env.FOREST_AUTH_SECRET)).not.toThrow();
|
|
453
|
+
// The forestadmin-client transforms the response from the API
|
|
454
|
+
// (e.g., first_name → firstName, id string → number, teams[0] → team)
|
|
455
|
+
const decoded = jsonwebtoken_1.default.decode(accessToken);
|
|
456
|
+
expect(decoded).toMatchObject({
|
|
457
|
+
id: 123,
|
|
458
|
+
email: 'user@example.com',
|
|
459
|
+
firstName: 'Test',
|
|
460
|
+
lastName: 'User',
|
|
461
|
+
team: 'Operations',
|
|
462
|
+
role: 'Admin',
|
|
463
|
+
permissionLevel: 'admin',
|
|
464
|
+
renderingId: 456,
|
|
465
|
+
tags: {},
|
|
466
|
+
serverToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXRhIjp7InJlbmRlcmluZ0lkIjo0NTZ9LCJzY29wZSI6Im1jcDpyZWFkIG1jcDp3cml0ZSIsImlhdCI6MjUyNDYwODAwMCwiZXhwIjoyNTI0NjExNjAwfQ.fake',
|
|
467
|
+
});
|
|
468
|
+
// JWT should also have iat and exp claims
|
|
469
|
+
expect(decoded.iat).toBeDefined();
|
|
470
|
+
expect(decoded.exp).toBeDefined();
|
|
471
|
+
// Verify refresh token structure
|
|
472
|
+
const refreshToken = response.body.refresh_token;
|
|
473
|
+
const decodedRefreshToken = jsonwebtoken_1.default.decode(refreshToken);
|
|
474
|
+
expect(decodedRefreshToken).toMatchObject({
|
|
475
|
+
type: 'refresh',
|
|
476
|
+
clientId: 'registered-client',
|
|
477
|
+
userId: 123,
|
|
478
|
+
renderingId: 456,
|
|
479
|
+
// The serverRefreshToken is the JWT returned from Forest Admin
|
|
480
|
+
serverRefreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjI1MjQ2MDgwMDAsImV4cCI6MjUyNTIxMjgwMH0.fake',
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
it('should exchange refresh token for new tokens', async () => {
|
|
484
|
+
mcpMockServer.clear();
|
|
485
|
+
// First, get initial tokens
|
|
486
|
+
const initialResponse = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
487
|
+
grant_type: 'authorization_code',
|
|
488
|
+
code: 'valid-auth-code',
|
|
489
|
+
redirect_uri: 'https://example.com/callback',
|
|
490
|
+
client_id: 'registered-client',
|
|
491
|
+
code_verifier: 'test-code-verifier',
|
|
492
|
+
});
|
|
493
|
+
expect(initialResponse.status).toBe(200);
|
|
494
|
+
const refreshToken = initialResponse.body.refresh_token;
|
|
495
|
+
// Clear mock to track new calls
|
|
496
|
+
mcpMockServer.clear();
|
|
497
|
+
// Now exchange refresh token for new tokens
|
|
498
|
+
const refreshResponse = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
499
|
+
grant_type: 'refresh_token',
|
|
500
|
+
refresh_token: refreshToken,
|
|
501
|
+
client_id: 'registered-client',
|
|
502
|
+
});
|
|
503
|
+
expect(refreshResponse.status).toBe(200);
|
|
504
|
+
expect(refreshResponse.body.access_token).toBeDefined();
|
|
505
|
+
expect(refreshResponse.body.refresh_token).toBeDefined();
|
|
506
|
+
expect(refreshResponse.body.token_type).toBe('Bearer');
|
|
507
|
+
// expires_in is calculated as exp - now from the JWT (duration in seconds)
|
|
508
|
+
expect(refreshResponse.body.expires_in).toBeGreaterThan(0);
|
|
509
|
+
// Verify the new access token is valid
|
|
510
|
+
const newAccessToken = refreshResponse.body.access_token;
|
|
511
|
+
expect(() => jsonwebtoken_1.default.verify(newAccessToken, process.env.FOREST_AUTH_SECRET)).not.toThrow();
|
|
512
|
+
// Verify token rotation: new refresh token is returned
|
|
513
|
+
const newRefreshToken = refreshResponse.body.refresh_token;
|
|
514
|
+
expect(newRefreshToken).toBeDefined();
|
|
515
|
+
// Verify it's a valid JWT with refresh token structure
|
|
516
|
+
const decodedNewRefresh = jsonwebtoken_1.default.decode(newRefreshToken);
|
|
517
|
+
expect(decodedNewRefresh.type).toBe('refresh');
|
|
518
|
+
expect(decodedNewRefresh.clientId).toBe('registered-client');
|
|
519
|
+
// Verify Forest Admin token endpoint was called with refresh_token grant
|
|
520
|
+
expect(mcpMockServer.fetch).toHaveBeenCalledWith('https://test.forestadmin.com/oauth/token', expect.objectContaining({
|
|
521
|
+
method: 'POST',
|
|
522
|
+
body: expect.stringContaining('"grant_type":"refresh_token"'),
|
|
523
|
+
}));
|
|
524
|
+
// Note: Token rotation is implemented - the new refresh token should be different
|
|
525
|
+
// However, since both requests use the same mock returning the same forest-server-refresh-token,
|
|
526
|
+
// the generated JWT will have similar claims but different iat/exp timestamps
|
|
527
|
+
const oldDecoded = jsonwebtoken_1.default.decode(refreshToken);
|
|
528
|
+
const newDecoded = jsonwebtoken_1.default.decode(newRefreshToken);
|
|
529
|
+
expect(newDecoded.iat).toBeGreaterThanOrEqual(oldDecoded.iat);
|
|
530
|
+
});
|
|
531
|
+
it('should return 400 for invalid refresh token', async () => {
|
|
532
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
533
|
+
grant_type: 'refresh_token',
|
|
534
|
+
refresh_token: 'invalid-token',
|
|
535
|
+
client_id: 'registered-client',
|
|
536
|
+
});
|
|
537
|
+
expect(response.status).toBe(400);
|
|
538
|
+
expect(response.body.error).toBeDefined();
|
|
539
|
+
});
|
|
540
|
+
it('should return 400 when refresh_token is missing for refresh_token grant', async () => {
|
|
541
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
542
|
+
grant_type: 'refresh_token',
|
|
543
|
+
client_id: 'registered-client',
|
|
544
|
+
});
|
|
545
|
+
expect(response.status).toBe(400);
|
|
546
|
+
});
|
|
547
|
+
it('should return 400 when client_id does not match refresh token', async () => {
|
|
548
|
+
// Create a refresh token for a different client
|
|
549
|
+
const refreshToken = jsonwebtoken_1.default.sign({
|
|
550
|
+
type: 'refresh',
|
|
551
|
+
clientId: 'different-client',
|
|
552
|
+
userId: 123,
|
|
553
|
+
renderingId: 456,
|
|
554
|
+
serverRefreshToken: 'forest-refresh-token',
|
|
555
|
+
}, process.env.FOREST_AUTH_SECRET, { expiresIn: '7d' });
|
|
556
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
557
|
+
grant_type: 'refresh_token',
|
|
558
|
+
refresh_token: refreshToken,
|
|
559
|
+
client_id: 'registered-client',
|
|
560
|
+
});
|
|
561
|
+
expect(response.status).toBe(400);
|
|
562
|
+
expect(response.body.error).toBeDefined();
|
|
563
|
+
});
|
|
564
|
+
describe('error handling', () => {
|
|
565
|
+
const setupErrorMock = (errorResponse, statusCode) => {
|
|
566
|
+
mcpMockServer.reset();
|
|
567
|
+
mcpMockServer
|
|
568
|
+
.get('/liana/environment', {
|
|
569
|
+
data: { id: '12345', attributes: { api_endpoint: 'https://api.example.com' } },
|
|
570
|
+
})
|
|
571
|
+
.get('/liana/forest-schema', {
|
|
572
|
+
data: [
|
|
573
|
+
{
|
|
574
|
+
id: 'users',
|
|
575
|
+
type: 'collections',
|
|
576
|
+
attributes: { name: 'users', fields: [{ field: 'id', type: 'Number' }] },
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
meta: {
|
|
580
|
+
liana: 'forest-express-sequelize',
|
|
581
|
+
liana_version: '9.0.0',
|
|
582
|
+
liana_features: null,
|
|
583
|
+
},
|
|
584
|
+
})
|
|
585
|
+
.get(/\/oauth\/register\/registered-client/, {
|
|
586
|
+
client_id: 'registered-client',
|
|
587
|
+
redirect_uris: ['https://example.com/callback'],
|
|
588
|
+
client_name: 'Test Client',
|
|
589
|
+
scope: 'mcp:read mcp:write',
|
|
590
|
+
})
|
|
591
|
+
.get(/\/oauth\/register\//, { error: 'Client not found' }, 404)
|
|
592
|
+
.post('/oauth/token', errorResponse, statusCode);
|
|
593
|
+
};
|
|
594
|
+
// Note: The implementation wraps all OAuth errors in InvalidRequestError,
|
|
595
|
+
// so the error code is always 'invalid_request' with the original error in the description
|
|
596
|
+
it('should return error when authorization code is invalid', async () => {
|
|
597
|
+
setupErrorMock({
|
|
598
|
+
error: 'invalid_grant',
|
|
599
|
+
error_description: 'The authorization code has expired or is invalid',
|
|
600
|
+
}, 400);
|
|
601
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
602
|
+
grant_type: 'authorization_code',
|
|
603
|
+
code: 'expired-or-invalid-code',
|
|
604
|
+
redirect_uri: 'https://example.com/callback',
|
|
605
|
+
client_id: 'registered-client',
|
|
606
|
+
code_verifier: 'test-code-verifier',
|
|
607
|
+
});
|
|
608
|
+
expect(response.status).toBe(400);
|
|
609
|
+
expect(response.body.error).toBe('invalid_request');
|
|
610
|
+
expect(response.body.error_description).toContain('Failed to exchange authorization code');
|
|
611
|
+
});
|
|
612
|
+
it('should return error when client authentication fails', async () => {
|
|
613
|
+
setupErrorMock({
|
|
614
|
+
error: 'invalid_client',
|
|
615
|
+
error_description: 'Client authentication failed',
|
|
616
|
+
}, 401);
|
|
617
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
618
|
+
grant_type: 'authorization_code',
|
|
619
|
+
code: 'some-code',
|
|
620
|
+
redirect_uri: 'https://example.com/callback',
|
|
621
|
+
client_id: 'registered-client',
|
|
622
|
+
code_verifier: 'test-code-verifier',
|
|
623
|
+
});
|
|
624
|
+
expect(response.status).toBe(400);
|
|
625
|
+
expect(response.body.error).toBe('invalid_request');
|
|
626
|
+
expect(response.body.error_description).toContain('Failed to exchange authorization code');
|
|
627
|
+
});
|
|
628
|
+
it('should return error when requested scope is invalid', async () => {
|
|
629
|
+
setupErrorMock({
|
|
630
|
+
error: 'invalid_scope',
|
|
631
|
+
error_description: 'The requested scope is invalid or unknown',
|
|
632
|
+
}, 400);
|
|
633
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
634
|
+
grant_type: 'authorization_code',
|
|
635
|
+
code: 'some-code',
|
|
636
|
+
redirect_uri: 'https://example.com/callback',
|
|
637
|
+
client_id: 'registered-client',
|
|
638
|
+
code_verifier: 'test-code-verifier',
|
|
639
|
+
});
|
|
640
|
+
expect(response.status).toBe(400);
|
|
641
|
+
expect(response.body.error).toBe('invalid_request');
|
|
642
|
+
expect(response.body.error_description).toContain('Failed to exchange authorization code');
|
|
643
|
+
});
|
|
644
|
+
it('should return error when client is not authorized', async () => {
|
|
645
|
+
setupErrorMock({
|
|
646
|
+
error: 'unauthorized_client',
|
|
647
|
+
error_description: 'The client is not authorized to use this grant type',
|
|
648
|
+
}, 403);
|
|
649
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
650
|
+
grant_type: 'authorization_code',
|
|
651
|
+
code: 'some-code',
|
|
652
|
+
redirect_uri: 'https://example.com/callback',
|
|
653
|
+
client_id: 'registered-client',
|
|
654
|
+
code_verifier: 'test-code-verifier',
|
|
655
|
+
});
|
|
656
|
+
expect(response.status).toBe(400);
|
|
657
|
+
expect(response.body.error).toBe('invalid_request');
|
|
658
|
+
expect(response.body.error_description).toContain('Failed to exchange authorization code');
|
|
659
|
+
});
|
|
660
|
+
it('should return error when Forest Admin server has internal error', async () => {
|
|
661
|
+
setupErrorMock({
|
|
662
|
+
error: 'server_error',
|
|
663
|
+
error_description: 'An unexpected error occurred on the server',
|
|
664
|
+
}, 500);
|
|
665
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
666
|
+
grant_type: 'authorization_code',
|
|
667
|
+
code: 'some-code',
|
|
668
|
+
redirect_uri: 'https://example.com/callback',
|
|
669
|
+
client_id: 'registered-client',
|
|
670
|
+
code_verifier: 'test-code-verifier',
|
|
671
|
+
});
|
|
672
|
+
expect(response.status).toBe(400);
|
|
673
|
+
expect(response.body.error).toBe('invalid_request');
|
|
674
|
+
expect(response.body.error_description).toContain('Failed to exchange authorization code');
|
|
675
|
+
});
|
|
676
|
+
it('should use default error description when not provided by Forest server', async () => {
|
|
677
|
+
setupErrorMock({ error: 'invalid_request' }, 400);
|
|
678
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
679
|
+
grant_type: 'authorization_code',
|
|
680
|
+
code: 'some-code',
|
|
681
|
+
redirect_uri: 'https://example.com/callback',
|
|
682
|
+
client_id: 'registered-client',
|
|
683
|
+
code_verifier: 'test-code-verifier',
|
|
684
|
+
});
|
|
685
|
+
expect(response.status).toBe(400);
|
|
686
|
+
expect(response.body.error).toBe('invalid_request');
|
|
687
|
+
expect(response.body.error_description).toContain('Failed to exchange authorization code');
|
|
688
|
+
});
|
|
689
|
+
it('should return error when Forest server returns error without error code', async () => {
|
|
690
|
+
setupErrorMock({ message: 'Something went wrong' }, 500);
|
|
691
|
+
const response = await (0, supertest_1.default)(mcpHttpServer).post('/oauth/token').type('form').send({
|
|
692
|
+
grant_type: 'authorization_code',
|
|
693
|
+
code: 'some-code',
|
|
694
|
+
redirect_uri: 'https://example.com/callback',
|
|
695
|
+
client_id: 'registered-client',
|
|
696
|
+
code_verifier: 'test-code-verifier',
|
|
697
|
+
});
|
|
698
|
+
expect(response.status).toBe(400);
|
|
699
|
+
expect(response.body.error).toBe('invalid_request');
|
|
700
|
+
expect(response.body.error_description).toContain('Failed to exchange authorization code');
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
/**
|
|
705
|
+
* Integration tests for the list tool
|
|
706
|
+
* Tests that the list tool is properly registered and accessible
|
|
707
|
+
*/
|
|
708
|
+
describe('List tool integration', () => {
|
|
709
|
+
let listServer;
|
|
710
|
+
let listHttpServer;
|
|
711
|
+
let listMockServer;
|
|
712
|
+
beforeAll(async () => {
|
|
713
|
+
process.env.FOREST_ENV_SECRET = 'test-env-secret';
|
|
714
|
+
process.env.FOREST_AUTH_SECRET = 'test-auth-secret';
|
|
715
|
+
process.env.FOREST_SERVER_URL = 'https://test.forestadmin.com';
|
|
716
|
+
process.env.AGENT_HOSTNAME = 'http://localhost:3310';
|
|
717
|
+
process.env.MCP_SERVER_PORT = '39330';
|
|
718
|
+
listMockServer = new mock_server_1.default();
|
|
719
|
+
listMockServer
|
|
720
|
+
.get('/liana/environment', {
|
|
721
|
+
data: { id: '12345', attributes: { api_endpoint: 'https://api.example.com' } },
|
|
722
|
+
})
|
|
723
|
+
.get('/liana/forest-schema', {
|
|
724
|
+
data: [
|
|
725
|
+
{
|
|
726
|
+
id: 'users',
|
|
727
|
+
type: 'collections',
|
|
728
|
+
attributes: { name: 'users', fields: [{ field: 'id', type: 'Number' }] },
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
id: 'products',
|
|
732
|
+
type: 'collections',
|
|
733
|
+
attributes: { name: 'products', fields: [{ field: 'name', type: 'String' }] },
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
meta: { liana: 'forest-express-sequelize', liana_version: '9.0.0', liana_features: null },
|
|
737
|
+
})
|
|
738
|
+
.get(/\/oauth\/register\/registered-client/, {
|
|
739
|
+
client_id: 'registered-client',
|
|
740
|
+
redirect_uris: ['https://example.com/callback'],
|
|
741
|
+
client_name: 'Test Client',
|
|
742
|
+
scope: 'mcp:read mcp:write',
|
|
743
|
+
})
|
|
744
|
+
.get(/\/oauth\/register\//, { error: 'Client not found' }, 404);
|
|
745
|
+
global.fetch = listMockServer.fetch;
|
|
746
|
+
listServer = new server_1.default();
|
|
747
|
+
listServer.run();
|
|
748
|
+
await new Promise(resolve => {
|
|
749
|
+
setTimeout(resolve, 500);
|
|
750
|
+
});
|
|
751
|
+
listHttpServer = listServer.httpServer;
|
|
752
|
+
});
|
|
753
|
+
afterAll(async () => {
|
|
754
|
+
await new Promise(resolve => {
|
|
755
|
+
if (listServer?.httpServer) {
|
|
756
|
+
listServer.httpServer.close(() => resolve());
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
resolve();
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
it('should have list tool registered in the MCP server', () => {
|
|
764
|
+
expect(listServer.mcpServer).toBeDefined();
|
|
765
|
+
// The tool should be registered during server initialization
|
|
766
|
+
// We verify this by checking the server started successfully
|
|
767
|
+
expect(listHttpServer).toBeDefined();
|
|
768
|
+
});
|
|
769
|
+
it('should require authentication to access /mcp endpoint', async () => {
|
|
770
|
+
const response = await (0, supertest_1.default)(listHttpServer).post('/mcp').send({
|
|
771
|
+
jsonrpc: '2.0',
|
|
772
|
+
method: 'tools/list',
|
|
773
|
+
id: 1,
|
|
774
|
+
});
|
|
775
|
+
// Without a valid bearer token, we should get an authentication error
|
|
776
|
+
expect(response.status).toBe(401);
|
|
777
|
+
});
|
|
778
|
+
it('should reject requests with invalid bearer token', async () => {
|
|
779
|
+
const response = await (0, supertest_1.default)(listHttpServer)
|
|
780
|
+
.post('/mcp')
|
|
781
|
+
.set('Authorization', 'Bearer invalid-token')
|
|
782
|
+
.send({
|
|
783
|
+
jsonrpc: '2.0',
|
|
784
|
+
method: 'tools/list',
|
|
785
|
+
id: 1,
|
|
786
|
+
});
|
|
787
|
+
expect(response.status).toBe(401);
|
|
788
|
+
});
|
|
789
|
+
it('should accept requests with valid bearer token and list available tools', async () => {
|
|
790
|
+
// Create a valid JWT token
|
|
791
|
+
const authSecret = process.env.FOREST_AUTH_SECRET || 'test-auth-secret';
|
|
792
|
+
const validToken = jsonwebtoken_1.default.sign({
|
|
793
|
+
id: 123,
|
|
794
|
+
email: 'user@example.com',
|
|
795
|
+
renderingId: 456,
|
|
796
|
+
}, authSecret, { expiresIn: '1h' });
|
|
797
|
+
const response = await (0, supertest_1.default)(listHttpServer)
|
|
798
|
+
.post('/mcp')
|
|
799
|
+
.set('Authorization', `Bearer ${validToken}`)
|
|
800
|
+
.set('Content-Type', 'application/json')
|
|
801
|
+
.set('Accept', 'application/json, text/event-stream')
|
|
802
|
+
.send({
|
|
803
|
+
jsonrpc: '2.0',
|
|
804
|
+
method: 'tools/list',
|
|
805
|
+
id: 1,
|
|
806
|
+
});
|
|
807
|
+
expect(response.status).toBe(200);
|
|
808
|
+
// The MCP SDK returns the response as text that needs to be parsed
|
|
809
|
+
// The response may be in JSON-RPC format or as a newline-delimited JSON stream
|
|
810
|
+
let responseData;
|
|
811
|
+
if (response.body && Object.keys(response.body).length > 0) {
|
|
812
|
+
responseData = response.body;
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
// Parse the text response - MCP returns Server-Sent Events format with "data: " prefix
|
|
816
|
+
const textResponse = response.text;
|
|
817
|
+
const lines = textResponse.split('\n').filter((line) => line.trim());
|
|
818
|
+
// Find the line with the actual JSON-RPC response (starts with "data: ")
|
|
819
|
+
const dataLine = lines.find((line) => line.startsWith('data: '));
|
|
820
|
+
if (dataLine) {
|
|
821
|
+
responseData = JSON.parse(dataLine.replace('data: ', ''));
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
responseData = JSON.parse(lines[lines.length - 1]);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
expect(responseData.jsonrpc).toBe('2.0');
|
|
828
|
+
expect(responseData.id).toBe(1);
|
|
829
|
+
expect(responseData.result).toBeDefined();
|
|
830
|
+
expect(responseData.result.tools).toBeDefined();
|
|
831
|
+
expect(Array.isArray(responseData.result.tools)).toBe(true);
|
|
832
|
+
// Verify the 'list' tool is registered
|
|
833
|
+
const listTool = responseData.result.tools.find((tool) => tool.name === 'list');
|
|
834
|
+
expect(listTool).toBeDefined();
|
|
835
|
+
expect(listTool.description).toBe('Retrieve a list of records from the specified collection.');
|
|
836
|
+
expect(listTool.inputSchema).toBeDefined();
|
|
837
|
+
expect(listTool.inputSchema.properties).toHaveProperty('collectionName');
|
|
838
|
+
expect(listTool.inputSchema.properties).toHaveProperty('search');
|
|
839
|
+
expect(listTool.inputSchema.properties).toHaveProperty('filters');
|
|
840
|
+
expect(listTool.inputSchema.properties).toHaveProperty('sort');
|
|
841
|
+
// Verify collectionName has enum with the collection names from the mocked schema
|
|
842
|
+
const collectionNameSchema = listTool.inputSchema.properties.collectionName;
|
|
843
|
+
expect(collectionNameSchema.type).toBe('string');
|
|
844
|
+
expect(collectionNameSchema.enum).toBeDefined();
|
|
845
|
+
expect(collectionNameSchema.enum).toEqual(['users', 'products']);
|
|
846
|
+
});
|
|
847
|
+
it('should create activity log with forestServerToken when calling list tool', async () => {
|
|
848
|
+
// This test verifies that the activity log API is called with the forestServerToken
|
|
849
|
+
// (the original Forest server token) and NOT the MCP JWT token.
|
|
850
|
+
// The forestServerToken is embedded in the MCP JWT during token exchange and extracted
|
|
851
|
+
// by verifyAccessToken into authInfo.extra.forestServerToken
|
|
852
|
+
const authSecret = process.env.FOREST_AUTH_SECRET || 'test-auth-secret';
|
|
853
|
+
const forestServerToken = 'original-forest-server-token-for-activity-log';
|
|
854
|
+
// Create MCP JWT with embedded serverToken (as done during OAuth token exchange)
|
|
855
|
+
const mcpToken = jsonwebtoken_1.default.sign({
|
|
856
|
+
id: 123,
|
|
857
|
+
email: 'user@example.com',
|
|
858
|
+
renderingId: 456,
|
|
859
|
+
serverToken: forestServerToken,
|
|
860
|
+
}, authSecret, { expiresIn: '1h' });
|
|
861
|
+
// Setup mock to capture the activity log API call and mock agent response
|
|
862
|
+
listMockServer.clear();
|
|
863
|
+
listMockServer
|
|
864
|
+
.post('/api/activity-logs-requests', { success: true })
|
|
865
|
+
.post('/forest/rpc', { result: [{ id: 1, name: 'Test' }] });
|
|
866
|
+
const response = await (0, supertest_1.default)(listHttpServer)
|
|
867
|
+
.post('/mcp')
|
|
868
|
+
.set('Authorization', `Bearer ${mcpToken}`)
|
|
869
|
+
.set('Content-Type', 'application/json')
|
|
870
|
+
.set('Accept', 'application/json, text/event-stream')
|
|
871
|
+
.send({
|
|
872
|
+
jsonrpc: '2.0',
|
|
873
|
+
method: 'tools/call',
|
|
874
|
+
params: {
|
|
875
|
+
name: 'list',
|
|
876
|
+
arguments: { collectionName: 'users' },
|
|
877
|
+
},
|
|
878
|
+
id: 2,
|
|
879
|
+
});
|
|
880
|
+
// The tool call should succeed (or fail on agent call, but activity log should be created first)
|
|
881
|
+
expect(response.status).toBe(200);
|
|
882
|
+
// Verify activity log API was called with the correct forestServerToken
|
|
883
|
+
// The mock fetch captures all calls as [url, options] tuples
|
|
884
|
+
const activityLogCall = listMockServer.fetch.mock.calls.find((call) => call[0] === 'https://test.forestadmin.com/api/activity-logs-requests');
|
|
885
|
+
expect(activityLogCall).toBeDefined();
|
|
886
|
+
expect(activityLogCall[1].headers).toMatchObject({
|
|
887
|
+
Authorization: `Bearer ${forestServerToken}`,
|
|
888
|
+
'Content-Type': 'application/json',
|
|
889
|
+
'Forest-Application-Source': 'MCP',
|
|
890
|
+
});
|
|
891
|
+
// Verify the body contains the correct data
|
|
892
|
+
const body = JSON.parse(activityLogCall[1].body);
|
|
893
|
+
expect(body.data.attributes.action).toBe('index');
|
|
894
|
+
expect(body.data.relationships.collection.data).toEqual({
|
|
895
|
+
id: 'users',
|
|
896
|
+
type: 'collections',
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmVyLnRlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvc2VydmVyLnRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFFQSxnRUFBd0M7QUFDeEMsMERBQWdDO0FBRWhDLHNEQUF1QztBQUN2QywyRUFBa0Q7QUFFbEQsU0FBUyxrQkFBa0IsQ0FBQyxNQUErQjtJQUN6RCxJQUFJLENBQUMsTUFBTTtRQUFFLE9BQU8sT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBRXRDLE9BQU8sSUFBSSxPQUFPLENBQUMsT0FBTyxDQUFDLEVBQUU7UUFDM0IsTUFBTSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUU7WUFDaEIsT0FBTyxFQUFFLENBQUM7UUFDWixDQUFDLENBQUMsQ0FBQztJQUNMLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQztBQUVEOzs7R0FHRztBQUNILFFBQVEsQ0FBQywwQkFBMEIsRUFBRSxHQUFHLEVBQUU7SUFDeEMsSUFBSSxNQUF1QixDQUFDO0lBQzVCLElBQUksV0FBOEIsQ0FBQztJQUNuQyxJQUFJLFdBQThCLENBQUM7SUFDbkMsSUFBSSxVQUFzQixDQUFDO0lBQzNCLE1BQU0sYUFBYSxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUM7SUFFbkMsU0FBUyxDQUFDLEdBQUcsRUFBRTtRQUNiLFdBQVcsR0FBRyxFQUFFLEdBQUcsT0FBTyxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQ2pDLE9BQU8sQ0FBQyxHQUFHLENBQUMsaUJBQWlCLEdBQUcsaUJBQWlCLENBQUM7UUFDbEQsT0FBTyxDQUFDLEdBQUcsQ0FBQyxrQkFBa0IsR0FBRyxrQkFBa0IsQ0FBQztRQUNwRCxPQUFPLENBQUMsR0FBRyxDQUFDLGlCQUFpQixHQUFHLDhCQUE4QixDQUFDO1FBQy9ELE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxHQUFHLHVCQUF1QixDQUFDO1FBRXJELHFDQUFxQztRQUNyQyxVQUFVLEdBQUcsSUFBSSxxQkFBVSxFQUFFLENB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
|