Skip to content

Commit 5da12b4

Browse files
authored
mcp: correctly fix authority-less MCP resources cannot be read by VS Code (microsoft#254003)
Closes microsoft#250905
1 parent 25ee562 commit 5da12b4

File tree

4 files changed

+26
-20
lines changed

4 files changed

+26
-20
lines changed

src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export class McpResourcePickHelper {
130130
return uri;
131131
}
132132

133-
this._notificationService.warn(localize('mcp.resource.template.notFound', "The resource {0} was not found.", McpResourceURI.toServer(uri).resourceURI.toString()));
133+
this._notificationService.warn(localize('mcp.resource.template.notFound', "The resource {0} was not found.", McpResourceURI.toServer(uri).resourceURL.toString()));
134134
return undefined;
135135
}
136136

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,11 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr
108108
watchedOnHandler = handler;
109109

110110
const token = callCts.value.token;
111-
handler.subscribe({ uri: resourceURI.toString(true) }, token).then(
111+
handler.subscribe({ uri: resourceURI.toString() }, token).then(
112112
() => {
113113
if (!token.isCancellationRequested) {
114114
watchListener.value = handler.onDidUpdateResource(e => {
115-
if (equalsUriPath(e.params.uri, resourceURI)) {
115+
if (equalsUrlPath(e.params.uri, resourceURI)) {
116116
this._onDidChangeFile.fire([{ resource: uri, type: FileChangeType.UPDATED }]);
117117
}
118118
});
@@ -147,7 +147,7 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr
147147
throw createFileSystemProviderError(`File is not a directory`, FileSystemProviderErrorCode.FileNotADirectory);
148148
}
149149

150-
const resourcePathParts = resourceURI.path.split('/');
150+
const resourcePathParts = resourceURI.pathname.split('/');
151151

152152
const output = new Map<string, FileType>();
153153
for (const content of contents) {
@@ -207,15 +207,15 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr
207207

208208
private _decodeURI(uri: URI) {
209209
let definitionId: string;
210-
let resourceURI: URI;
210+
let resourceURL: URL;
211211
try {
212-
({ definitionId, resourceURI } = McpResourceURI.toServer(uri));
212+
({ definitionId, resourceURL } = McpResourceURI.toServer(uri));
213213
} catch (e) {
214214
throw createFileSystemProviderError(String(e), FileSystemProviderErrorCode.FileNotFound);
215215
}
216216

217-
if (resourceURI.path.endsWith('/')) {
218-
resourceURI = resourceURI.with({ path: resourceURI.path.slice(0, -1) });
217+
if (resourceURL.pathname.endsWith('/')) {
218+
resourceURL.pathname = resourceURL.pathname.slice(0, -1);
219219
}
220220

221221
const server = this._mcpService.servers.get().find(s => s.definition.id === definitionId);
@@ -228,25 +228,25 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr
228228
throw createFileSystemProviderError(`MCP server ${definitionId} does not support resources`, FileSystemProviderErrorCode.FileNotFound);
229229
}
230230

231-
return { definitionId, resourceURI, server };
231+
return { definitionId, resourceURI: resourceURL, server };
232232
}
233233

234234
private async _readURI(uri: URI, token?: CancellationToken) {
235235
const { resourceURI, server } = this._decodeURI(uri);
236-
const res = await McpServer.callOn(server, r => r.readResource({ uri: resourceURI.toString(true) }, token), token);
236+
const res = await McpServer.callOn(server, r => r.readResource({ uri: resourceURI.toString() }, token), token);
237237

238238
return {
239239
contents: res.contents,
240240
resourceURI,
241-
forSameURI: res.contents.filter(c => equalsUriPath(c.uri, resourceURI)),
241+
forSameURI: res.contents.filter(c => equalsUrlPath(c.uri, resourceURI)),
242242
};
243243
}
244244
}
245245

246-
function equalsUriPath(a: string, b: URI): boolean {
246+
function equalsUrlPath(a: string, b: URL): boolean {
247247
// MCP doesn't specify either way, but underlying systems may can be case-sensitive.
248248
// It's better to treat case-sensitive paths as case-insensitive than vise-versa.
249-
return equalsIgnoreCase(URI.parse(a).path, b.path);
249+
return equalsIgnoreCase(new URL(a).pathname, b.pathname);
250250
}
251251

252252
function contentToBuffer(content: MCP.TextResourceContents | MCP.BlobResourceContents): Uint8Array {

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,7 @@ export namespace McpResourceURI {
678678
});
679679
}
680680

681-
export function toServer(uri: URI | string): { definitionId: string; resourceURI: URI } {
681+
export function toServer(uri: URI | string): { definitionId: string; resourceURL: URL } {
682682
if (typeof uri === 'string') {
683683
uri = URI.parse(uri);
684684
}
@@ -690,13 +690,16 @@ export namespace McpResourceURI {
690690
throw new Error(`Invalid MCP resource URI: ${uri.toString()}`);
691691
}
692692
const [, serverScheme, authority, ...path] = parts;
693+
694+
// URI cannot correctly stringify empty authorities (#250905) so we use URL instead to construct
695+
const url = new URL(`${serverScheme}://${authority.toLowerCase() === emptyAuthorityPlaceholder ? '' : authority}`);
696+
url.pathname = path.length ? ('/' + path.join('/')) : '';
697+
url.search = uri.query;
698+
url.hash = uri.fragment;
699+
693700
return {
694701
definitionId: decodeHex(uri.authority).toString(),
695-
resourceURI: uri.with({
696-
scheme: serverScheme,
697-
authority: authority.toLowerCase() === emptyAuthorityPlaceholder ? '' : authority,
698-
path: path.length ? ('/' + path.join('/')) : '',
699-
}),
702+
resourceURL: url,
700703
};
701704
}
702705

src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ suite('MCP Types', () => {
1515
const from = McpResourceURI.fromServer({ label: '', id: 'my-id' }, uri);
1616
const to = McpResourceURI.toServer(from);
1717
assert.strictEqual(to.definitionId, 'my-id');
18-
assert.strictEqual(to.resourceURI.toString(true), uri, `expected to round trip ${uri}`);
18+
assert.strictEqual(to.resourceURL.toString(), uri, `expected to round trip ${uri}`);
1919
};
2020

2121
roundTrip('file:///path/to/file.txt');
2222
roundTrip('custom-scheme://my-path/to/resource.txt');
2323
roundTrip('custom-scheme://my-path');
2424
roundTrip('custom-scheme://my-path/');
2525
roundTrip('custom-scheme://my-path/?with=query&params=here');
26+
27+
roundTrip('custom-scheme:///my-path');
28+
roundTrip('custom-scheme:///my-path/foo/?with=query&params=here');
2629
});
2730
});

0 commit comments

Comments
 (0)