8
8
ListToolsResult ,
9
9
} from "@modelcontextprotocol/sdk/types.js" ;
10
10
import { checkFeatureActive , mcpError } from "./util.js" ;
11
- import { SERVER_FEATURES , ServerFeature } from "./types.js" ;
11
+ import { ClientConfig , SERVER_FEATURES , ServerFeature } from "./types.js" ;
12
12
import { availableTools } from "./tools/index.js" ;
13
13
import { ServerTool , ServerToolContext } from "./tool.js" ;
14
14
import { configstore } from "../configstore.js" ;
@@ -23,12 +23,14 @@ import { loadRC } from "../rc.js";
23
23
import { EmulatorHubClient } from "../emulator/hubClient.js" ;
24
24
25
25
const SERVER_VERSION = "0.0.1" ;
26
- const PROJECT_ROOT_KEY = "mcp.projectRoot" ;
27
26
28
27
const cmd = new Command ( "experimental:mcp" ) . before ( requireAuth ) ;
29
28
30
29
export 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 ;
32
34
server : Server ;
33
35
activeFeatures ?: ServerFeature [ ] ;
34
36
detectedFeatures ?: ServerFeature [ ] ;
@@ -37,11 +39,12 @@ export class FirebaseMcpServer {
37
39
38
40
constructor ( options : { activeFeatures ?: ServerFeature [ ] ; projectRoot ?: string } ) {
39
41
this . activeFeatures = options . activeFeatures ;
42
+ this . startupRoot = options . projectRoot || process . env . PROJECT_ROOT ;
40
43
this . server = new Server ( { name : "firebase" , version : SERVER_VERSION } ) ;
41
44
this . server . registerCapabilities ( { tools : { listChanged : true } } ) ;
42
45
this . server . setRequestHandler ( ListToolsRequestSchema , this . mcpListTools . bind ( this ) ) ;
43
46
this . server . setRequestHandler ( CallToolRequestSchema , this . mcpCallTool . bind ( this ) ) ;
44
- this . server . oninitialized = ( ) => {
47
+ this . server . oninitialized = async ( ) => {
45
48
const clientInfo = this . server . getClientVersion ( ) ;
46
49
this . clientInfo = clientInfo ;
47
50
if ( clientInfo ?. name ) {
@@ -50,15 +53,48 @@ export class FirebaseMcpServer {
50
53
mcp_client_version : clientInfo . version ,
51
54
} ) ;
52
55
}
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
+ }
53
62
} ;
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 ( ) ;
59
64
this . detectActiveFeatures ( ) ;
60
65
}
61
66
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
+
62
98
async detectActiveFeatures ( ) : Promise < ServerFeature [ ] > {
63
99
if ( this . detectedFeatures ?. length ) return this . detectedFeatures ; // memoized
64
100
const options = await this . resolveOptions ( ) ;
@@ -97,21 +133,14 @@ export class FirebaseMcpServer {
97
133
}
98
134
99
135
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 ;
109
138
this . detectedFeatures = undefined ; // reset detected features
110
139
void this . server . sendToolListChanged ( ) ;
111
140
}
112
141
113
142
async resolveOptions ( ) : Promise < Partial < Options > > {
114
- const options : Partial < Options > = { cwd : this . projectRoot } ;
143
+ const options : Partial < Options > = { cwd : this . cachedProjectRoot } ;
115
144
await cmd . prepare ( options ) ;
116
145
return options ;
117
146
}
@@ -129,7 +158,7 @@ export class FirebaseMcpServer {
129
158
}
130
159
131
160
async mcpListTools ( ) : Promise < ListToolsResult > {
132
- if ( ! this . activeFeatures ) await this . detectActiveFeatures ( ) ;
161
+ await Promise . all ( [ this . detectActiveFeatures ( ) , this . detectProjectRoot ( ) ] ) ;
133
162
const hasActiveProject = ! ! ( await this . getProjectId ( ) ) ;
134
163
await trackGA4 ( "mcp_list_tools" , {
135
164
mcp_client_name : this . clientInfo ?. name ,
@@ -138,7 +167,7 @@ export class FirebaseMcpServer {
138
167
return {
139
168
tools : this . availableTools . map ( ( t ) => t . mcp ) ,
140
169
_meta : {
141
- projectRoot : this . projectRoot ,
170
+ projectRoot : this . cachedProjectRoot ,
142
171
projectDetected : hasActiveProject ,
143
172
authenticatedUser : await this . getAuthenticatedUser ( ) ,
144
173
activeFeatures : this . activeFeatures ,
@@ -148,6 +177,7 @@ export class FirebaseMcpServer {
148
177
}
149
178
150
179
async mcpCallTool ( request : CallToolRequest ) : Promise < CallToolResult > {
180
+ await this . detectProjectRoot ( ) ;
151
181
const toolName = request . params . name ;
152
182
const toolArgs = request . params . arguments ;
153
183
const tool = this . getTool ( toolName ) ;
@@ -158,7 +188,7 @@ export class FirebaseMcpServer {
158
188
if ( tool . mcp . _meta ?. requiresAuth && ! accountEmail ) return mcpAuthError ( ) ;
159
189
if ( tool . mcp . _meta ?. requiresProject && ! projectId ) return NO_PROJECT_ERROR ;
160
190
161
- const options = { projectDir : this . projectRoot , cwd : this . projectRoot } ;
191
+ const options = { projectDir : this . cachedProjectRoot , cwd : this . cachedProjectRoot } ;
162
192
const toolsCtx : ServerToolContext = {
163
193
projectId : projectId ,
164
194
host : this ,
0 commit comments