11import { delimiter } from "node:path" ;
2+ import {
3+ type McpToolApprovalState ,
4+ type McpToolApprovals ,
5+ sanitizeMcpServerName ,
6+ } from "@posthog/agent/adapters/claude/mcp/tool-metadata" ;
27import { getLlmGatewayUrl } from "@posthog/agent/posthog-api" ;
38import { inject , injectable } from "inversify" ;
49import { MAIN_TOKENS } from "../../di/tokens" ;
@@ -10,6 +15,15 @@ import type { Credentials } from "./schemas";
1015
1116const log = logger . scope ( "agent-auth-adapter" ) ;
1217
18+ const VALID_APPROVAL_STATES = new Set ( [
19+ "approved" ,
20+ "needs_approval" ,
21+ "do_not_use" ,
22+ ] ) ;
23+ function isValidApprovalState ( value : string ) : value is McpToolApprovalState {
24+ return VALID_APPROVAL_STATES . has ( value ) ;
25+ }
26+
1327export interface AcpMcpServer {
1428 name : string ;
1529 type : "http" ;
@@ -24,6 +38,15 @@ export interface AgentPosthogConfig {
2438 projectId : number ;
2539}
2640
41+ /** Reference linking an MCP tool key back to its server installation for backend updates. */
42+ export interface McpToolInstallationRef {
43+ installationId : string ;
44+ toolName : string ;
45+ }
46+
47+ /** Maps MCP tool keys (e.g. `mcp__server__tool`) to their installation reference. */
48+ export type McpToolInstallations = Record < string , McpToolInstallationRef > ;
49+
2750interface ConfigureProcessEnvInput {
2851 credentials : Credentials ;
2952 mockNodeDir : string ;
@@ -51,7 +74,11 @@ export class AgentAuthAdapter {
5174 } ;
5275 }
5376
54- async buildMcpServers ( credentials : Credentials ) : Promise < AcpMcpServer [ ] > {
77+ async buildMcpServers ( credentials : Credentials ) : Promise < {
78+ servers : AcpMcpServer [ ] ;
79+ toolApprovals : McpToolApprovals ;
80+ toolInstallations : McpToolInstallations ;
81+ } > {
5582 const servers : AcpMcpServer [ ] = [ ] ;
5683 const mcpUrl = this . getPostHogMcpUrl ( credentials . apiHost ) ;
5784 // Warm the token so authenticatedFetch() has something cached, but do not
@@ -95,7 +122,10 @@ export class AgentAuthAdapter {
95122 } ) ;
96123 }
97124
98- return servers ;
125+ const { approvals : toolApprovals , toolInstallations } =
126+ await this . fetchMcpToolApprovals ( credentials , installations ) ;
127+
128+ return { servers, toolApprovals, toolInstallations } ;
99129 }
100130
101131 async ensureGatewayProxy ( apiHost : string ) : Promise < string > {
@@ -154,6 +184,96 @@ export class AgentAuthAdapter {
154184 return host . endsWith ( "/" ) ? host . slice ( 0 , - 1 ) : host ;
155185 }
156186
187+ async updateMcpToolApproval (
188+ credentials : Credentials ,
189+ installationId : string ,
190+ toolName : string ,
191+ approvalState : McpToolApprovalState ,
192+ ) : Promise < void > {
193+ const baseUrl = this . getPostHogApiBaseUrl ( credentials . apiHost ) ;
194+ const url = `${ baseUrl } /api/environments/${ credentials . projectId } /mcp_server_installations/${ installationId } /tools/${ encodeURIComponent ( toolName ) } /` ;
195+ const response = await this . authService . authenticatedFetch ( fetch , url , {
196+ method : "PATCH" ,
197+ headers : { "Content-Type" : "application/json" } ,
198+ body : JSON . stringify ( { approval_state : approvalState } ) ,
199+ } ) ;
200+ if ( ! response . ok ) {
201+ throw new Error (
202+ `Failed to update MCP tool approval (${ response . status } ) for ${ toolName } on installation ${ installationId } ` ,
203+ ) ;
204+ }
205+ }
206+
207+ private async fetchMcpToolApprovals (
208+ credentials : Credentials ,
209+ installations : Array < {
210+ id : string ;
211+ url : string ;
212+ name : string ;
213+ display_name : string ;
214+ } > ,
215+ ) : Promise < {
216+ approvals : McpToolApprovals ;
217+ toolInstallations : McpToolInstallations ;
218+ } > {
219+ const baseUrl = this . getPostHogApiBaseUrl ( credentials . apiHost ) ;
220+ const approvals : McpToolApprovals = { } ;
221+ const toolInstallations : McpToolInstallations = { } ;
222+
223+ const results = await Promise . allSettled (
224+ installations . map ( async ( installation ) => {
225+ const serverName = sanitizeMcpServerName (
226+ installation . name || installation . display_name || installation . url ,
227+ ) ;
228+ const toolsUrl = `${ baseUrl } /api/environments/${ credentials . projectId } /mcp_server_installations/${ installation . id } /tools/` ;
229+
230+ const response = await this . authService . authenticatedFetch (
231+ fetch ,
232+ toolsUrl ,
233+ { headers : { "Content-Type" : "application/json" } } ,
234+ ) ;
235+ if ( ! response . ok ) return [ ] ;
236+
237+ const data = ( await response . json ( ) ) as {
238+ results ?: Array < {
239+ tool_name : string ;
240+ approval_state ?: string ;
241+ } > ;
242+ } ;
243+ return ( data . results ?? [ ] ) . map ( ( tool ) => ( {
244+ serverName,
245+ installationId : installation . id ,
246+ toolName : tool . tool_name ,
247+ approvalState : tool . approval_state ,
248+ } ) ) ;
249+ } ) ,
250+ ) ;
251+
252+ for ( const result of results ) {
253+ if ( result . status !== "fulfilled" ) {
254+ log . warn ( "Failed to fetch tool approvals for an installation" , {
255+ error :
256+ result . reason instanceof Error
257+ ? result . reason . message
258+ : String ( result . reason ) ,
259+ } ) ;
260+ continue ;
261+ }
262+ for ( const tool of result . value ) {
263+ const key = `mcp__${ tool . serverName } __${ tool . toolName } ` ;
264+ if ( tool . approvalState && isValidApprovalState ( tool . approvalState ) ) {
265+ approvals [ key ] = tool . approvalState ;
266+ }
267+ toolInstallations [ key ] = {
268+ installationId : tool . installationId ,
269+ toolName : tool . toolName ,
270+ } ;
271+ }
272+ }
273+
274+ return { approvals, toolInstallations } ;
275+ }
276+
157277 private async fetchMcpInstallations ( credentials : Credentials ) : Promise <
158278 Array < {
159279 id : string ;
0 commit comments