@@ -14,13 +14,36 @@ import { LoggerBase, LogId } from "./logger.js";
14
14
export const jsonExportFormat = z . enum ( [ "relaxed" , "canonical" ] ) ;
15
15
export type JSONExportFormat = z . infer < typeof jsonExportFormat > ;
16
16
17
- export type Export = {
18
- name : string ;
19
- uri : string ;
20
- path : string ;
21
- createdAt : number ;
17
+ type StoredExport = {
18
+ exportName : string ;
19
+ exportURI : string ;
20
+ exportPath : string ;
21
+ exportCreatedAt : number ;
22
22
} ;
23
23
24
+ /**
25
+ * Ideally just exportName and exportURI should be made publicly available but
26
+ * we also make exportPath available because the export tool, also returns the
27
+ * exportPath in its response when the MCP server is running connected to stdio
28
+ * transport. The reasoning behind this is that a few clients, Cursor in
29
+ * particular, as of the date of this writing (7 August 2025) cannot refer to
30
+ * resource URIs which means they have no means to access the exported resource.
31
+ * As of this writing, majority of the usage of our MCP server is behind STDIO
32
+ * transport so we can assume that for most of the usages, if not all, the MCP
33
+ * server will be running on the same machine as of the MCP client and thus we
34
+ * can provide the local path to export so that these clients which do not still
35
+ * support parsing resource URIs, can still work with the exported data. We
36
+ * expect for clients to catch up and implement referencing resource URIs at
37
+ * which point it would be safe to remove the `exportPath` from the publicly
38
+ * exposed properties of an export.
39
+ *
40
+ * The editors that we would like to watch out for are Cursor and Windsurf as
41
+ * they don't yet support working with Resource URIs.
42
+ *
43
+ * Ref Cursor: https://forum.cursor.com/t/cursor-mcp-resource-feature-support/50987
44
+ * JIRA: https://jira.mongodb.org/browse/MCP-104 */
45
+ type AvailableExport = Pick < StoredExport , "exportName" | "exportURI" | "exportPath" > ;
46
+
24
47
export type SessionExportsManagerConfig = Pick <
25
48
UserConfig ,
26
49
"exportsPath" | "exportTimeoutMs" | "exportCleanupIntervalMs"
@@ -32,7 +55,7 @@ type SessionExportsManagerEvents = {
32
55
} ;
33
56
34
57
export class SessionExportsManager extends EventEmitter < SessionExportsManagerEvents > {
35
- private sessionExports : Record < Export [ "name "] , Export > = { } ;
58
+ private sessionExports : Record < StoredExport [ "exportName "] , StoredExport > = { } ;
36
59
private exportsCleanupInProgress : boolean = false ;
37
60
private exportsCleanupInterval : NodeJS . Timeout ;
38
61
private exportsDirectoryPath : string ;
@@ -50,10 +73,14 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
50
73
) ;
51
74
}
52
75
53
- public get availableExports ( ) : Export [ ] {
54
- return Object . values ( this . sessionExports ) . filter (
55
- ( { createdAt } ) => ! isExportExpired ( createdAt , this . config . exportTimeoutMs )
56
- ) ;
76
+ public get availableExports ( ) : AvailableExport [ ] {
77
+ return Object . values ( this . sessionExports )
78
+ . filter ( ( { exportCreatedAt : createdAt } ) => ! isExportExpired ( createdAt , this . config . exportTimeoutMs ) )
79
+ . map ( ( { exportName, exportURI, exportPath } ) => ( {
80
+ exportName,
81
+ exportURI,
82
+ exportPath,
83
+ } ) ) ;
57
84
}
58
85
59
86
public async close ( ) : Promise < void > {
@@ -69,27 +96,21 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
69
96
}
70
97
}
71
98
72
- public async readExport ( exportName : string ) : Promise < {
73
- content : string ;
74
- exportURI : string ;
75
- } > {
99
+ public async readExport ( exportName : string ) : Promise < string > {
76
100
try {
77
101
const exportNameWithExtension = validateExportName ( exportName ) ;
78
102
const exportHandle = this . sessionExports [ exportNameWithExtension ] ;
79
103
if ( ! exportHandle ) {
80
104
throw new Error ( "Requested export has either expired or does not exist!" ) ;
81
105
}
82
106
83
- const { path : exportPath , uri , createdAt } = exportHandle ;
107
+ const { exportPath, exportCreatedAt } = exportHandle ;
84
108
85
- if ( isExportExpired ( createdAt , this . config . exportTimeoutMs ) ) {
109
+ if ( isExportExpired ( exportCreatedAt , this . config . exportTimeoutMs ) ) {
86
110
throw new Error ( "Requested export has expired!" ) ;
87
111
}
88
112
89
- return {
90
- exportURI : uri ,
91
- content : await fs . readFile ( exportPath , "utf8" ) ,
92
- } ;
113
+ return await fs . readFile ( exportPath , "utf8" ) ;
93
114
} catch ( error ) {
94
115
this . logger . error ( {
95
116
id : LogId . exportReadError ,
@@ -111,10 +132,7 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
111
132
input : FindCursor ;
112
133
exportName : string ;
113
134
jsonExportFormat : JSONExportFormat ;
114
- } ) : Promise < {
115
- exportURI : string ;
116
- exportPath : string ;
117
- } > {
135
+ } ) : Promise < AvailableExport > {
118
136
try {
119
137
const exportNameWithExtension = validateExportName ( ensureExtension ( exportName , "json" ) ) ;
120
138
const exportURI = `exported-data://${ encodeURIComponent ( exportNameWithExtension ) } ` ;
@@ -130,6 +148,7 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
130
148
await pipeline ( [ inputStream , ejsonDocStream , outputStream ] ) ;
131
149
pipeSuccessful = true ;
132
150
return {
151
+ exportName,
133
152
exportURI,
134
153
exportPath : exportFilePath ,
135
154
} ;
@@ -150,10 +169,10 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
150
169
void input . close ( ) ;
151
170
if ( pipeSuccessful ) {
152
171
this . sessionExports [ exportNameWithExtension ] = {
153
- name : exportNameWithExtension ,
154
- createdAt : Date . now ( ) ,
155
- path : exportFilePath ,
156
- uri : exportURI ,
172
+ exportName : exportNameWithExtension ,
173
+ exportCreatedAt : Date . now ( ) ,
174
+ exportPath : exportFilePath ,
175
+ exportURI : exportURI ,
157
176
} ;
158
177
this . emit ( "export-available" , exportURI ) ;
159
178
}
@@ -211,11 +230,11 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
211
230
this . exportsCleanupInProgress = true ;
212
231
const exportsForCleanup = { ...this . sessionExports } ;
213
232
try {
214
- for ( const { path : exportPath , createdAt , uri , name } of Object . values ( exportsForCleanup ) ) {
215
- if ( isExportExpired ( createdAt , this . config . exportTimeoutMs ) ) {
216
- delete this . sessionExports [ name ] ;
233
+ for ( const { exportPath, exportCreatedAt , exportURI , exportName } of Object . values ( exportsForCleanup ) ) {
234
+ if ( isExportExpired ( exportCreatedAt , this . config . exportTimeoutMs ) ) {
235
+ delete this . sessionExports [ exportName ] ;
217
236
await this . silentlyRemoveExport ( exportPath ) ;
218
- this . emit ( "export-expired" , uri ) ;
237
+ this . emit ( "export-expired" , exportURI ) ;
219
238
}
220
239
}
221
240
} catch ( error ) {
0 commit comments