src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts

316 LOC · 260 covered · 56 uncovered · 51 ranges · 19 concepts · 17 introducers · 9 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/base/common/buffer.ts · 499 LOCcommon/buffer.tssrc/vs/base/common/observableInternal/observables/derived.ts · 194 LOCobservables/derived.tssrc/vs/base/common/observableInternal/observables/observableValue.ts · 129 LOCobservables/observableVa…src/vs/base/common/observableInternal/reactions/autorun.ts · 294 LOCreactions/autorun.tssrc/vs/base/common/observableInternal/utils/promise.ts · 171 LOCutils/promise.tssrc/vs/platform/log/common/log.ts · 926 LOCcommon/log.tssrc/vs/platform/webContentExtractor/common/webContentExtractor.ts · 63 LOCcommon/webContentExtract…src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts · 393 LOCcommon/mcpConfiguration.…src/vs/workbench/contrib/mcp/common/mcpDevMode.ts · 213 LOCcommon/mcpDevMode.tssrc/vs/workbench/contrib/mcp/common/mcpServer.ts · 1539 LOCcommon/mcpServer.tssrc/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts · 769 LOCcommon/mcpServerRequestH…src/vs/workbench/contrib/mcp/common/mcpService.ts · 297 LOCcommon/mcpService.tssrc/vs/workbench/contrib/mcp/common/mcpTypes.ts · 1102 LOCcommon/mcpTypes.tssrc/vs/workbench/contrib/mcp/common/mcpTypesUtils.ts · 180 LOCcommon/mcpTypesUtils.tssrc/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts · 247 LOCcommon/mcpRegistryTypes.…mcpResourceFilesystem.ts ×3 · 45 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×1 · 2 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem stat returns directory information|occurrence=1 · 0 introduced LOCmcpResourceFilesystem.te…mcpResourceFilesystem.ts ×2 · 23 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×1 · 5 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×1 · 2 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem reads a basic file|occurrence=1 · 0 introduced LOCmcpResourceFilesystem.te…mcpResourceFilesystem.ts ×1 · 2 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×1 · 2 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×1 · 1 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×3 · 10 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×3 · 4 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×2 · 3 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×2 · 5 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×5 · 32 introduced LOCmcpResourceFilesystem.ts…mcpResourceFilesystem.ts ×1 · 5 introduced LOCmcpResourceFilesystem.ts…mcpServer.ts ×36 · 246 introduced LOCmcpServer.ts ×36mcpResourceFilesystem.ts ×4 · 8 introduced LOCmcpResourceFilesystem.ts…mcpServer.ts ×23 · 552 introduced LOCmcpServer.ts ×23mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem read blob resource|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem read blob resource|occurrence=1mcpResourceFilesystem.te…mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem readdir returns directory contents|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem readdir returns directory contents|occurrence=1mcpResourceFilesystem.te…mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem readdir throws when reading a file as directory|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem readdir throws when reading a file as directory|occurrence=1mcpResourceFilesystem.te…mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem reads a basic file|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem reads a basic file|occurrence=1mcpResourceFilesystem.te…mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem stat returns directory information|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem stat returns directory information|occurrence=1mcpResourceFilesystem.te…mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem stat returns file information|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem stat returns file information|occurrence=1mcpResourceFilesystem.te…mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem stat throws FileNotFound for nonexistent resources|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem stat throws FileNotFound for nonexistent resources|occurrence=1mcpResourceFilesystem.te…mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem throws error for write operations|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem throws error for write operations|occurrence=1mcpResourceFilesystem.te…mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem watch file emits change events|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test|title=Workbench - MCP - ResourceFilesystem watch file emits change events|occurrence=1mcpResourceFilesystem.te…Focused file · src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts · 316 LOCcommon/mcpResourceFilesy…

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- mcpServer.ts ×23
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 { sumBy } from '../../../../base/common/arrays.js';
7 > import { disposableTimeout } from '../../../../base/common/async.js';
8 > import { decodeBase64, VSBuffer } from '../../../../base/common/buffer.js';
9 > import { CancellationToken, CancellationTokenPool, CancellationTokenSource } from '../../../../base/common/cancellation.js';
10 > import { Emitter, Event } from '../../../../base/common/event.js';
11 > import { Lazy } from '../../../../base/common/lazy.js';
12 > import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
13 > import { ResourceMap } from '../../../../base/common/map.js';
14 > import { autorun } from '../../../../base/common/observable.js';
15 > import { newWriteableStream, ReadableStreamEvents } from '../../../../base/common/stream.js';
16 > import { equalsIgnoreCase } from '../../../../base/common/strings.js';
17 > import { URI } from '../../../../base/common/uri.js';
18 > import { createFileSystemProviderError, FileChangeType, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileReadStreamOptions, IFileService, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileWriteOptions, IStat, IWatchOptions } from '../../../../platform/files/common/files.js';
19 > import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
20 > import { IWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js';
21 > import { IWorkbenchContribution } from '../../../common/contributions.js';
22 > import { McpServer } from './mcpServer.js';
23 > import { McpServerRequestHandler } from './mcpServerRequestHandler.js';
24 > import { IMcpService, McpCapability, McpResourceURI } from './mcpTypes.js';
25 > import { canLoadMcpNetworkResourceDirectly } from './mcpTypesUtils.js';
26 > import { MCP } from './modelContextProtocol.js';
27 >
28 > const MOMENTARY_CACHE_DURATION = 3000;
29 >
30 > interface IReadData {
31 > contents: (MCP.TextResourceContents | MCP.BlobResourceContents)[];
32 > resourceURI: URL;
33 > forSameURI: (MCP.TextResourceContents | MCP.BlobResourceContents)[];
34 > }
35 >
36 > export class McpResourceFilesystem extends Disposable implements IWorkbenchContribution,
37 > IFileSystemProviderWithFileReadWriteCapability,
38 > IFileSystemProviderWithFileAtomicReadCapability,
39 > IFileSystemProviderWithFileReadStreamCapability {
40 > /** Defer getting the MCP service since this is a BlockRestore and no need to make it unnecessarily. */
41 > private readonly _mcpServiceLazy = new Lazy(() => this._instantiationService.invokeFunction(a => a.get(IMcpService)));
42 >
43 > /**
44 > * For many file operations we re-read the resources quickly (e.g. stat
45 > * before reading the file) and would prefer to avoid spamming the MCP
46 > * with multiple reads. This is a very short-duration cache
47 > * to solve that.
48 > */
49 > private readonly _momentaryCache = new ResourceMap<{ pool: CancellationTokenPool; promise: Promise<IReadData> }>();
50 >
51 > private get _mcpService() {
52 > return this._mcpServiceLazy.value;
53 > }
54 >
55 > public readonly onDidChangeCapabilities = Event.None;
56 >
57 > private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
58 > public readonly onDidChangeFile = this._onDidChangeFile.event;
59 >
60 > public readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.None
61 > | FileSystemProviderCapabilities.Readonly
62 > | FileSystemProviderCapabilities.PathCaseSensitive
63 > | FileSystemProviderCapabilities.FileReadStream
64 > | FileSystemProviderCapabilities.FileAtomicRead
65 > | FileSystemProviderCapabilities.FileReadWrite;
66 >
67 > constructor(
68 > @IInstantiationService private readonly _instantiationService: IInstantiationService,
69 > @IFileService private readonly _fileService: IFileService,
70 > @IWebContentExtractorService private readonly _webContentExtractorService: IWebContentExtractorService,
71 > ) {
72 > super();
73 > this._register(this._fileService.registerProvider(McpResourceURI.scheme, this));
74 > }
75 >
76 > //#region Filesystem API
77 >
78 > public async readFile(resource: URI): Promise<Uint8Array> {
79 > return this._readFile(resource); mcpResourceFilesystem.ts ×3
80 > }
82 > public readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
83 const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
84
85 this._readFile(resource, token).then(
86 data => {
87 if (opts.position) {
88 data = data.slice(opts.position);
89 }
90
91 if (opts.length) {
92 data = data.slice(0, opts.length);
93 }
94
95 stream.end(data);
96 },
97 err => stream.error(err),
98 );
99
100 return stream;
101 }
103 > public watch(uri: URI, _opts: IWatchOptions): IDisposable {
104 > const { resourceURI, server } = this._decodeURI(uri); mcpResourceFilesystem.ts ×3
105 > const cap = server.capabilities.get();
106 > if (cap !== undefined && !(cap & McpCapability.ResourcesSubscribe)) {
107 return Disposable.None;
108 }
110 > server.start();
111 >
112 > const store = new DisposableStore();
113 > let watchedOnHandler: McpServerRequestHandler | undefined;
114 > const watchListener = store.add(new MutableDisposable());
115 > const callCts = store.add(new MutableDisposable<CancellationTokenSource>());
116 > store.add(autorun(reader => {
117 > const connection = server.connection.read(reader);
118 > if (!connection) {
119 > return;
120 > }
121 >
122 > const handler = connection.handler.read(reader);
123 > if (!handler || watchedOnHandler === handler) {
124 > return;
125 > }
126 >
127 > callCts.value?.dispose(true);
128 > callCts.value = new CancellationTokenSource();
129 > watchedOnHandler = handler;
130 >
131 > const token = callCts.value.token;
132 > handler.subscribe({ uri: resourceURI.toString() }, token).then(
133 > () => {
134 > if (!token.isCancellationRequested) {
135 > watchListener.value = handler.onDidUpdateResource(e => {
136 > if (equalsUrlPath(e.params.uri, resourceURI)) {
137 > this._onDidChangeFile.fire([{ resource: uri, type: FileChangeType.UPDATED }]);
138 > }
139 > });
140 > }
141 > }, err => {
142 handler.logger.warn(`Failed to subscribe to resource changes for ${resourceURI}: ${err}`);
143 watchedOnHandler = undefined;
144 },
146 > }));
147 >
148 > return store;
149 > }
151 > public async stat(resource: URI): Promise<IStat> {
152 > const { forSameURI, contents } = await this._readURI(resource); mcpResourceFilesystem.ts ×2
153 > if (!contents.length) {
154 > throw createFileSystemProviderError(`File not found`, FileSystemProviderErrorCode.FileNotFound); mcpResourceFilesystem.ts ×1
155 > }
157 > return {
158 > ctime: 0,
159 > mtime: 0,
160 > size: sumBy(contents, c => contentToBuffer(c).byteLength),
161 > type: forSameURI.length ? FileType.File : FileType.Directory, mcpResourceFilesystem.ts ×2
162 > };
163 > }
165 > public async readdir(resource: URI): Promise<[string, FileType][]> {
166 > const { forSameURI, contents, resourceURI } = await this._readURI(resource); mcpResourceFilesystem.ts ×2
167 > if (forSameURI.length > 0) {
168 > throw createFileSystemProviderError(`File is not a directory`, FileSystemProviderErrorCode.FileNotADirectory); mcpResourceFilesystem.ts ×1
169 > }
170 > const resourcePathParts = resourceURI.pathname.split('/'); mcpResourceFilesystem.ts ×2
171 >
172 > const output = new Map<string, FileType>();
173 > for (const content of contents) {
174 > const contentURI = URI.parse(content.uri);
175 > const contentPathParts = contentURI.path.split('/');
176 >
177 > // Skip contents that are not in the same directory
178 > if (contentPathParts.length <= resourcePathParts.length || !resourcePathParts.every((part, index) => equalsIgnoreCase(part, contentPathParts[index]))) {
179 continue;
180 }
182 > // nested resource in a directory, just emit a directory to output
183 > else if (contentPathParts.length > resourcePathParts.length + 1) {
184 > output.set(contentPathParts[resourcePathParts.length], FileType.Directory);
185 > }
186 >
187 > else {
188 > // resource in the same directory, emit the file
189 > const name = contentPathParts[contentPathParts.length - 1];
190 > output.set(name, contentToBuffer(content).byteLength > 0 ? FileType.File : FileType.Directory);
191 > }
192 > }
193 >
194 > return [...output];
197 > public mkdir(resource: URI): Promise<void> {
198 > throw createFileSystemProviderError('write is not supported', FileSystemProviderErrorCode.NoPermissions); mcpResourceFilesystem.ts ×4
199 > }
200 > public writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> { mcpServer.ts ×23
201 > throw createFileSystemProviderError('write is not supported', FileSystemProviderErrorCode.NoPermissions); mcpResourceFilesystem.ts ×4
202 > }
203 > public delete(resource: URI, opts: IFileDeleteOptions): Promise<void> { mcpServer.ts ×23
204 > throw createFileSystemProviderError('delete is not supported', FileSystemProviderErrorCode.NoPermissions); mcpResourceFilesystem.ts ×4
205 > }
206 > public rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> { mcpServer.ts ×23
207 > throw createFileSystemProviderError('rename is not supported', FileSystemProviderErrorCode.NoPermissions); mcpResourceFilesystem.ts ×4
208 > }
210 > //#endregion
211 >
212 > private async _readFile(resource: URI, token?: CancellationToken): Promise<Uint8Array> {
213 > const { forSameURI, contents } = await this._readURI(resource); mcpResourceFilesystem.ts ×3
214 >
215 > // MCP does not distinguish between files and directories, and says that
216 > // servers should just return multiple when 'reading' a directory.
217 > if (!forSameURI.length) {
218 if (!contents.length) {
219 throw createFileSystemProviderError(`File not found`, FileSystemProviderErrorCode.FileNotFound);
220 } else {
221 throw createFileSystemProviderError(`File is a directory`, FileSystemProviderErrorCode.FileIsADirectory);
222 }
223 }
225 > return contentToBuffer(forSameURI[0]);
226 > }
228 > private _decodeURI(uri: URI) {
229 > let definitionId: string; mcpServer.ts ×36
230 > let resourceURL: URL;
231 > try {
232 > ({ definitionId, resourceURL } = McpResourceURI.toServer(uri));
233 > } catch (e) {
234 throw createFileSystemProviderError(String(e), FileSystemProviderErrorCode.FileNotFound);
235 }
237 > if (resourceURL.pathname.endsWith('/')) {
238 > resourceURL.pathname = resourceURL.pathname.slice(0, -1); mcpResourceFilesystem.ts ×1
239 > }
241 > const server = this._mcpService.servers.get().find(s => s.definition.id === definitionId);
242 > if (!server) {
243 throw createFileSystemProviderError(`MCP server ${definitionId} not found`, FileSystemProviderErrorCode.FileNotFound);
244 }
246 > const cap = server.capabilities.get();
247 > if (cap !== undefined && !(cap & McpCapability.Resources)) {
248 throw createFileSystemProviderError(`MCP server ${definitionId} does not support resources`, FileSystemProviderErrorCode.FileNotFound);
249 }
251 > return { definitionId, resourceURI: resourceURL, server };
252 > }
254 > private async _readURI(uri: URI, token?: CancellationToken) {
255 > const cached = this._momentaryCache.get(uri); mcpResourceFilesystem.ts ×5
256 > if (cached) {
257 cached.pool.add(token || CancellationToken.None);
258 return cached.promise;
259 }
261 > const pool = this._store.add(new CancellationTokenPool());
262 > pool.add(token || CancellationToken.None);
263 >
264 > const promise = this._readURIInner(uri, pool.token);
265 > this._momentaryCache.set(uri, { pool, promise });
266 >
267 > const disposable = this._store.add(disposableTimeout(() => {
268 this._momentaryCache.delete(uri);
269 this._store.delete(disposable);
270 this._store.delete(pool);
271 > }, MOMENTARY_CACHE_DURATION)); mcpResourceFilesystem.ts ×5
272 >
273 > return promise;
274 > }
276 > private async _readURIInner(uri: URI, token?: CancellationToken): Promise<IReadData> {
277 > const { resourceURI, server } = this._decodeURI(uri); mcpResourceFilesystem.ts ×5
278 > const matchedServer = this._mcpService.servers.get().find(s => s.definition.id === server.definition.id);
279 >
280 > //check for http/https resources and use web content extractor service to fetch the contents.
281 > if (canLoadMcpNetworkResourceDirectly(resourceURI, matchedServer)) {
282 const extractURI = URI.parse(resourceURI.toString());
283 const result = (await this._webContentExtractorService.extract([extractURI], { followRedirects: false })).at(0);
284 if (result?.status === 'ok') {
285 return {
286 contents: [{ uri: resourceURI.toString(), text: result.result }],
287 resourceURI,
288 forSameURI: [{ uri: resourceURI.toString(), text: result.result }]
289 };
290 }
291 }
293 > const res = await McpServer.callOn(server, r => r.readResource({ uri: resourceURI.toString() }, token), token);
294 > return {
295 > contents: res.contents,
296 > resourceURI,
297 > forSameURI: res.contents.filter(c => equalsUrlPath(c.uri, resourceURI))
298 > };
299 > }
301 >
302 > function equalsUrlPath(a: string, b: URL): boolean { mcpResourceFilesystem.ts ×1
303 > // MCP doesn't specify either way, but underlying systems may can be case-sensitive.
304 > // It's better to treat case-sensitive paths as case-insensitive than vise-versa.
305 > return equalsIgnoreCase(new URL(a).pathname, b.pathname);
306 > }
308 > function contentToBuffer(content: MCP.TextResourceContents | MCP.BlobResourceContents): Uint8Array { mcpResourceFilesystem.ts ×3
309 > if ('text' in content) {
310 > return VSBuffer.fromString(content.text).buffer; mcpResourceFilesystem.ts ×1
311 > } else if ('blob' in content) { mcpResourceFilesystem.ts ×3
312 > return decodeBase64(content.blob).buffer; mcpResourceFilesystem.ts ×1
313 > } else {
314 throw createFileSystemProviderError('Unknown content type', FileSystemProviderErrorCode.Unknown);
315 }