Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as assert from 'assert';
7
import { Barrier, timeout } from '../../../../../base/common/async.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
10
import { NullCommandService } from '../../../../../platform/commands/test/common/nullCommandService.js';
11
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
12
import { FileChangeType, FileSystemProviderErrorCode, FileType, IFileChange, IFileService } from '../../../../../platform/files/common/files.js';
13
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
14
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
15
import { ILoggerService, NullLogService } from '../../../../../platform/log/common/log.js';
16
import { IProductService } from '../../../../../platform/product/common/productService.js';
17
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
18
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
19
import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';
20
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
21
import { TestContextService, TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';
22
import { IMcpRegistry } from '../../common/mcpRegistryTypes.js';
23
import { McpResourceFilesystem } from '../../common/mcpResourceFilesystem.js';
24
import { McpService } from '../../common/mcpService.js';
25
import { IMcpService } from '../../common/mcpTypes.js';
26
import { MCP } from '../../common/modelContextProtocol.js';
27
import { TestMcpMessageTransport, TestMcpRegistry } from './mcpRegistryTypes.js';
28
29
30
suite('Workbench - MCP - ResourceFilesystem', () => {
31
32
const ds = ensureNoDisposablesAreLeakedInTestSuite();
33
34
let transport: TestMcpMessageTransport;
35
let fs: McpResourceFilesystem;
36
37
setup(() => {
38
const services = new ServiceCollection(
39
[IFileService, { registerProvider: () => { } }],
40
[IStorageService, ds.add(new TestStorageService())],
41
[ILoggerService, ds.add(new TestLoggerService())],
42
[IWorkspaceContextService, new TestContextService()],
43
[ITelemetryService, NullTelemetryService],
44
[IProductService, TestProductService],
45
);
46
47
const parentInsta1 = ds.add(new TestInstantiationService(services));
48
const registry = new TestMcpRegistry(parentInsta1);
49
50
const parentInsta2 = ds.add(parentInsta1.createChild(new ServiceCollection([IMcpRegistry, registry])));
51
const mcpService = ds.add(new McpService(parentInsta2, registry, new NullLogService(), {} as any, NullCommandService, new TestConfigurationService()));
52
mcpService.updateCollectedServers();
53
54
const instaService = ds.add(parentInsta2.createChild(new ServiceCollection(
55
[IMcpRegistry, registry],
56
[IMcpService, mcpService],
57
)));
58
59
fs = ds.add(instaService.createInstance(McpResourceFilesystem));
60
61
transport = ds.add(new TestMcpMessageTransport());
62
registry.makeTestTransport = () => transport;
63
});
64
65
test('reads a basic file', async () => {
66
transport.setResponder('resources/read', msg => {
67
assert.strictEqual(msg.params.uri, 'custom://hello/world.txt');
68
return {
69
id: msg.id,
70
jsonrpc: '2.0',
71
result: {
72
contents: [{ uri: msg.params.uri, text: 'Hello World' }],
73
} satisfies MCP.ReadResourceResult
74
};
75
});
76
77
const response = await fs.readFile(URI.parse('mcp-resource://746573742D736572766572/custom/hello/world.txt'));
78
assert.strictEqual(new TextDecoder().decode(response), 'Hello World');
79
});
80
81
test('stat returns file information', async () => {
82
transport.setResponder('resources/read', msg => {
83
assert.strictEqual(msg.params.uri, 'custom://hello/world.txt');
84
return {
85
id: msg.id,
86
jsonrpc: '2.0',
87
result: {
88
contents: [{ uri: msg.params.uri, text: 'Hello World' }],
89
} satisfies MCP.ReadResourceResult
90
};
91
});
92
93
const fileStats = await fs.stat(URI.parse('mcp-resource://746573742D736572766572/custom/hello/world.txt'));
94
assert.strictEqual(fileStats.type, FileType.File);
95
assert.strictEqual(fileStats.size, 'Hello World'.length);
96
});
97
98
test('stat returns directory information', async () => {
99
transport.setResponder('resources/read', msg => {
100
assert.strictEqual(msg.params.uri, 'custom://hello');
101
return {
102
id: msg.id,
103
jsonrpc: '2.0',
104
result: {
105
contents: [
106
{ uri: 'custom://hello/file1.txt', text: 'File 1' },
107
{ uri: 'custom://hello/file2.txt', text: 'File 2' },
108
],
109
} satisfies MCP.ReadResourceResult
110
};
111
});
112
113
const dirStats = await fs.stat(URI.parse('mcp-resource://746573742D736572766572/custom/hello/'));
114
assert.strictEqual(dirStats.type, FileType.Directory);
115
// Size should be sum of all file contents in the directory
116
assert.strictEqual(dirStats.size, 'File 1'.length + 'File 2'.length);
117
});
118
119
test('stat throws FileNotFound for nonexistent resources', async () => {
120
transport.setResponder('resources/read', msg => {
121
return {
122
id: msg.id,
123
jsonrpc: '2.0',
124
result: {
125
contents: [],
126
} satisfies MCP.ReadResourceResult
127
};
128
});
129
130
await assert.rejects(
131
() => fs.stat(URI.parse('mcp-resource://746573742D736572766572/custom/nonexistent.txt')),
132
(err: any) => err.code === FileSystemProviderErrorCode.FileNotFound
133
);
134
});
135
136
test('readdir returns directory contents', async () => {
137
transport.setResponder('resources/read', msg => {
138
assert.strictEqual(msg.params.uri, 'custom://hello/dir');
139
return {
140
id: msg.id,
141
jsonrpc: '2.0',
142
result: {
143
contents: [
144
{ uri: 'custom://hello/dir/file1.txt', text: 'File 1' },
145
{ uri: 'custom://hello/dir/file2.txt', text: 'File 2' },
146
{ uri: 'custom://hello/dir/subdir/file3.txt', text: 'File 3' },
147
],
148
} satisfies MCP.ReadResourceResult
149
};
150
});
151
152
const dirEntries = await fs.readdir(URI.parse('mcp-resource://746573742D736572766572/custom/hello/dir/'));
153
assert.deepStrictEqual(dirEntries, [
154
['file1.txt', FileType.File],
155
['file2.txt', FileType.File],
156
['subdir', FileType.Directory],
157
]);
158
});
159
160
test('readdir throws when reading a file as directory', async () => {
161
transport.setResponder('resources/read', msg => {
162
return {
163
id: msg.id,
164
jsonrpc: '2.0',
165
result: {
166
contents: [{ uri: msg.params.uri, text: 'This is a file' }],
167
} satisfies MCP.ReadResourceResult
168
};
169
});
170
171
await assert.rejects(
172
() => fs.readdir(URI.parse('mcp-resource://746573742D736572766572/custom/hello/file.txt')),
173
(err: any) => err.code === FileSystemProviderErrorCode.FileNotADirectory
174
);
175
});
176
177
test('watch file emits change events', async () => {
178
// Set up the responder for resource reading
179
transport.setResponder('resources/read', msg => {
180
return {
181
id: msg.id,
182
jsonrpc: '2.0',
183
result: {
184
contents: [{ uri: msg.params.uri, text: 'File content' }],
185
} satisfies MCP.ReadResourceResult
186
};
187
});
188
189
const didSubscribe = new Barrier();
190
191
// Set up the responder for resource subscription
192
transport.setResponder('resources/subscribe', msg => {
193
didSubscribe.open();
194
return {
195
id: msg.id,
196
jsonrpc: '2.0',
197
result: {},
198
};
199
});
200
201
const uri = URI.parse('mcp-resource://746573742D736572766572/custom/hello/file.txt');
202
const fileChanges: IFileChange[] = [];
203
204
// Create a listener for file change events
205
const disposable = fs.onDidChangeFile(events => {
206
fileChanges.push(...events);
207
});
208
209
// Start watching the file
210
const watchDisposable = fs.watch(uri, { excludes: [], recursive: false });
211
212
// Simulate a file update notification from the server
213
await didSubscribe.wait();
214
await timeout(10); // wait for listeners to attach
215
216
transport.simulateReceiveMessage({
217
jsonrpc: '2.0',
218
method: 'notifications/resources/updated',
219
params: {
220
uri: 'custom://hello/file.txt',
221
},
222
});
223
transport.simulateReceiveMessage({
224
jsonrpc: '2.0',
225
method: 'notifications/resources/updated',
226
params: {
227
uri: 'custom://hello/unrelated.txt',
228
},
229
});
230
231
// Check that we received a file change event
232
assert.strictEqual(fileChanges.length, 1);
233
assert.strictEqual(fileChanges[0].type, FileChangeType.UPDATED);
234
assert.strictEqual(fileChanges[0].resource.toString(), uri.toString());
235
236
// Clean up
237
disposable.dispose();
238
watchDisposable.dispose();
239
});
240
241
test('read blob resource', async () => {
242
const blobBase64 = 'SGVsbG8gV29ybGQgYXMgQmxvYg=='; // "Hello World as Blob" in base64
243
244
transport.setResponder('resources/read', msg => {
245
assert.strictEqual(msg.params.uri, 'custom://hello/blob.bin');
246
return {
247
id: msg.id,
248
jsonrpc: '2.0',
249
result: {
250
contents: [{ uri: msg.params.uri, blob: blobBase64 }],
251
} satisfies MCP.ReadResourceResult
252
};
253
});
254
255
const response = await fs.readFile(URI.parse('mcp-resource://746573742D736572766572/custom/hello/blob.bin'));
256
assert.strictEqual(new TextDecoder().decode(response), 'Hello World as Blob');
257
});
258
259
test('throws error for write operations', async () => {
260
const uri = URI.parse('mcp-resource://746573742D736572766572/custom/hello/file.txt');
261
262
await assert.rejects(
263
async () => fs.writeFile(uri, new Uint8Array(), { create: true, overwrite: true, atomic: false, unlock: false }),
264
(err: any) => err.code === FileSystemProviderErrorCode.NoPermissions
265
);
266
267
await assert.rejects(
268
async () => fs.delete(uri, { recursive: false, useTrash: false, atomic: false }),
269
(err: any) => err.code === FileSystemProviderErrorCode.NoPermissions
270
);
271
272
await assert.rejects(
273
async () => fs.mkdir(uri),
274
(err: any) => err.code === FileSystemProviderErrorCode.NoPermissions
275
);
276
277
await assert.rejects(
278
async () => fs.rename(uri, URI.parse('mcp-resource://746573742D736572766572/custom/hello/newfile.txt'), { overwrite: false }),
279
(err: any) => err.code === FileSystemProviderErrorCode.NoPermissions
280
);
281
});
282
});
283
284