88 ListToolsResult ,
99} from "@modelcontextprotocol/sdk/types.js" ;
1010import { checkFeatureActive , mcpError } from "./util.js" ;
11- import { SERVER_FEATURES , ServerFeature } from "./types.js" ;
11+ import { ClientConfig , SERVER_FEATURES , ServerFeature } from "./types.js" ;
1212import { availableTools } from "./tools/index.js" ;
1313import { ServerTool , ServerToolContext } from "./tool.js" ;
1414import { configstore } from "../configstore.js" ;
@@ -23,12 +23,14 @@ import { loadRC } from "../rc.js";
2323import { EmulatorHubClient } from "../emulator/hubClient.js" ;
2424
2525const SERVER_VERSION = "0.0.1" ;
26- const PROJECT_ROOT_KEY = "mcp.projectRoot" ;
2726
2827const cmd = new Command ( "experimental:mcp" ) . before ( requireAuth ) ;
2928
3029export class FirebaseMcpServer {
31- projectRoot ?: string ;
30+ private _ready : boolean = false ;
31+ private _readyPromises : { resolve : ( ) => void ; reject : ( err : unknown ) => void } [ ] = [ ] ;
32+ startupRoot ?: string ;
33+ cachedProjectRoot ?: string ;
3234 server : Server ;
3335 activeFeatures ?: ServerFeature [ ] ;
3436 detectedFeatures ?: ServerFeature [ ] ;
@@ -37,11 +39,12 @@ export class FirebaseMcpServer {
3739
3840 constructor ( options : { activeFeatures ?: ServerFeature [ ] ; projectRoot ?: string } ) {
3941 this . activeFeatures = options . activeFeatures ;
42+ this . startupRoot = options . projectRoot || process . env . PROJECT_ROOT ;
4043 this . server = new Server ( { name : "firebase" , version : SERVER_VERSION } ) ;
4144 this . server . registerCapabilities ( { tools : { listChanged : true } } ) ;
4245 this . server . setRequestHandler ( ListToolsRequestSchema , this . mcpListTools . bind ( this ) ) ;
4346 this . server . setRequestHandler ( CallToolRequestSchema , this . mcpCallTool . bind ( this ) ) ;
44- this . server . oninitialized = ( ) => {
47+ this . server . oninitialized = async ( ) => {
4548 const clientInfo = this . server . getClientVersion ( ) ;
4649 this . clientInfo = clientInfo ;
4750 if ( clientInfo ?. name ) {
@@ -50,15 +53,48 @@ export class FirebaseMcpServer {
5053 mcp_client_version : clientInfo . version ,
5154 } ) ;
5255 }
56+ if ( ! this . clientInfo ?. name ) this . clientInfo = { name : "<unknown-client>" } ;
57+
58+ this . _ready = true ;
59+ while ( this . _readyPromises . length ) {
60+ this . _readyPromises . pop ( ) ?. resolve ( ) ;
61+ }
5362 } ;
54- this . projectRoot =
55- options . projectRoot ??
56- ( configstore . get ( PROJECT_ROOT_KEY ) as string ) ??
57- process . env . PROJECT_ROOT ??
58- process . cwd ( ) ;
63+ this . detectProjectRoot ( ) ;
5964 this . detectActiveFeatures ( ) ;
6065 }
6166
67+ /** Wait until initialization has finished. */
68+ ready ( ) {
69+ if ( this . _ready ) return Promise . resolve ( ) ;
70+ return new Promise ( ( resolve , reject ) => {
71+ this . _readyPromises . push ( { resolve : resolve as ( ) => void , reject } ) ;
72+ } ) ;
73+ }
74+
75+ private get clientConfigKey ( ) {
76+ return `mcp.clientConfigs.${ this . clientInfo ?. name || "<unknown-client>" } :${ this . startupRoot || process . cwd ( ) } ` ;
77+ }
78+
79+ getStoredClientConfig ( ) : ClientConfig {
80+ return configstore . get ( this . clientConfigKey ) || { } ;
81+ }
82+
83+ updateStoredClientConfig ( update : Partial < ClientConfig > ) {
84+ const config = configstore . get ( this . clientConfigKey ) || { } ;
85+ const newConfig = { ...config , ...update } ;
86+ configstore . set ( this . clientConfigKey , newConfig ) ;
87+ return newConfig ;
88+ }
89+
90+ async detectProjectRoot ( ) : Promise < string > {
91+ await this . ready ( ) ;
92+ if ( this . cachedProjectRoot ) return this . cachedProjectRoot ;
93+ const storedRoot = this . getStoredClientConfig ( ) . projectRoot ;
94+ this . cachedProjectRoot = storedRoot || this . startupRoot || process . cwd ( ) ;
95+ return this . cachedProjectRoot ;
96+ }
97+
6298 async detectActiveFeatures ( ) : Promise < ServerFeature [ ] > {
6399 if ( this . detectedFeatures ?. length ) return this . detectedFeatures ; // memoized
64100 const options = await this . resolveOptions ( ) ;
@@ -97,21 +133,14 @@ export class FirebaseMcpServer {
97133 }
98134
99135 setProjectRoot ( newRoot : string | null ) : void {
100- if ( newRoot === null ) {
101- configstore . delete ( PROJECT_ROOT_KEY ) ;
102- this . projectRoot = process . env . PROJECT_ROOT || process . cwd ( ) ;
103- void this . server . sendToolListChanged ( ) ;
104- return ;
105- }
106-
107- configstore . set ( PROJECT_ROOT_KEY , newRoot ) ;
108- this . projectRoot = newRoot ;
136+ this . updateStoredClientConfig ( { projectRoot : newRoot } ) ;
137+ this . cachedProjectRoot = newRoot || undefined ;
109138 this . detectedFeatures = undefined ; // reset detected features
110139 void this . server . sendToolListChanged ( ) ;
111140 }
112141
113142 async resolveOptions ( ) : Promise < Partial < Options > > {
114- const options : Partial < Options > = { cwd : this . projectRoot } ;
143+ const options : Partial < Options > = { cwd : this . cachedProjectRoot } ;
115144 await cmd . prepare ( options ) ;
116145 return options ;
117146 }
@@ -129,7 +158,7 @@ export class FirebaseMcpServer {
129158 }
130159
131160 async mcpListTools ( ) : Promise < ListToolsResult > {
132- if ( ! this . activeFeatures ) await this . detectActiveFeatures ( ) ;
161+ await Promise . all ( [ this . detectActiveFeatures ( ) , this . detectProjectRoot ( ) ] ) ;
133162 const hasActiveProject = ! ! ( await this . getProjectId ( ) ) ;
134163 await trackGA4 ( "mcp_list_tools" , {
135164 mcp_client_name : this . clientInfo ?. name ,
@@ -138,7 +167,7 @@ export class FirebaseMcpServer {
138167 return {
139168 tools : this . availableTools . map ( ( t ) => t . mcp ) ,
140169 _meta : {
141- projectRoot : this . projectRoot ,
170+ projectRoot : this . cachedProjectRoot ,
142171 projectDetected : hasActiveProject ,
143172 authenticatedUser : await this . getAuthenticatedUser ( ) ,
144173 activeFeatures : this . activeFeatures ,
@@ -148,6 +177,7 @@ export class FirebaseMcpServer {
148177 }
149178
150179 async mcpCallTool ( request : CallToolRequest ) : Promise < CallToolResult > {
180+ await this . detectProjectRoot ( ) ;
151181 const toolName = request . params . name ;
152182 const toolArgs = request . params . arguments ;
153183 const tool = this . getTool ( toolName ) ;
@@ -158,7 +188,7 @@ export class FirebaseMcpServer {
158188 if ( tool . mcp . _meta ?. requiresAuth && ! accountEmail ) return mcpAuthError ( ) ;
159189 if ( tool . mcp . _meta ?. requiresProject && ! projectId ) return NO_PROJECT_ERROR ;
160190
161- const options = { projectDir : this . projectRoot , cwd : this . projectRoot } ;
191+ const options = { projectDir : this . cachedProjectRoot , cwd : this . cachedProjectRoot } ;
162192 const toolsCtx : ServerToolContext = {
163193 projectId : projectId ,
164194 host : this ,
0 commit comments