@@ -14,13 +14,36 @@ import { LoggerBase, LogId } from "./logger.js";
1414export const jsonExportFormat = z . enum ( [ "relaxed" , "canonical" ] ) ;
1515export type JSONExportFormat = z . infer < typeof jsonExportFormat > ;
1616
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 ;
2222} ;
2323
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+
2447export type SessionExportsManagerConfig = Pick <
2548 UserConfig ,
2649 "exportsPath" | "exportTimeoutMs" | "exportCleanupIntervalMs"
@@ -32,7 +55,7 @@ type SessionExportsManagerEvents = {
3255} ;
3356
3457export class SessionExportsManager extends EventEmitter < SessionExportsManagerEvents > {
35- private sessionExports : Record < Export [ "name "] , Export > = { } ;
58+ private sessionExports : Record < StoredExport [ "exportName "] , StoredExport > = { } ;
3659 private exportsCleanupInProgress : boolean = false ;
3760 private exportsCleanupInterval : NodeJS . Timeout ;
3861 private exportsDirectoryPath : string ;
@@ -50,10 +73,14 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
5073 ) ;
5174 }
5275
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+ } ) ) ;
5784 }
5885
5986 public async close ( ) : Promise < void > {
@@ -69,27 +96,21 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
6996 }
7097 }
7198
72- public async readExport ( exportName : string ) : Promise < {
73- content : string ;
74- exportURI : string ;
75- } > {
99+ public async readExport ( exportName : string ) : Promise < string > {
76100 try {
77101 const exportNameWithExtension = validateExportName ( exportName ) ;
78102 const exportHandle = this . sessionExports [ exportNameWithExtension ] ;
79103 if ( ! exportHandle ) {
80104 throw new Error ( "Requested export has either expired or does not exist!" ) ;
81105 }
82106
83- const { path : exportPath , uri , createdAt } = exportHandle ;
107+ const { exportPath, exportCreatedAt } = exportHandle ;
84108
85- if ( isExportExpired ( createdAt , this . config . exportTimeoutMs ) ) {
109+ if ( isExportExpired ( exportCreatedAt , this . config . exportTimeoutMs ) ) {
86110 throw new Error ( "Requested export has expired!" ) ;
87111 }
88112
89- return {
90- exportURI : uri ,
91- content : await fs . readFile ( exportPath , "utf8" ) ,
92- } ;
113+ return await fs . readFile ( exportPath , "utf8" ) ;
93114 } catch ( error ) {
94115 this . logger . error ( {
95116 id : LogId . exportReadError ,
@@ -111,10 +132,7 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
111132 input : FindCursor ;
112133 exportName : string ;
113134 jsonExportFormat : JSONExportFormat ;
114- } ) : Promise < {
115- exportURI : string ;
116- exportPath : string ;
117- } > {
135+ } ) : Promise < AvailableExport > {
118136 try {
119137 const exportNameWithExtension = validateExportName ( ensureExtension ( exportName , "json" ) ) ;
120138 const exportURI = `exported-data://${ encodeURIComponent ( exportNameWithExtension ) } ` ;
@@ -130,6 +148,7 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
130148 await pipeline ( [ inputStream , ejsonDocStream , outputStream ] ) ;
131149 pipeSuccessful = true ;
132150 return {
151+ exportName,
133152 exportURI,
134153 exportPath : exportFilePath ,
135154 } ;
@@ -150,10 +169,10 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
150169 void input . close ( ) ;
151170 if ( pipeSuccessful ) {
152171 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 ,
157176 } ;
158177 this . emit ( "export-available" , exportURI ) ;
159178 }
@@ -211,11 +230,11 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
211230 this . exportsCleanupInProgress = true ;
212231 const exportsForCleanup = { ...this . sessionExports } ;
213232 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 ] ;
217236 await this . silentlyRemoveExport ( exportPath ) ;
218- this . emit ( "export-expired" , uri ) ;
237+ this . emit ( "export-expired" , exportURI ) ;
219238 }
220239 }
221240 } catch ( error ) {
0 commit comments