33"""
44
55import asyncio
6+ import contextlib
67import itertools
78import json
89import os
910from dataclasses import dataclass
1011from pathlib import Path
1112from typing import ClassVar
1213
13- from src .udbpy .fileutil import mkdtemp
14+ from src .udbpy .fileutil import mkdtemp , mkstemp
1415
1516from .agents import BaseAgent
1617from .assets import FRAMING_PROMPT , SYSTEM_PROMPT
@@ -78,23 +79,23 @@ async def ask(self, question: str, port: int, tools: list[str]) -> str:
7879 if self .log_level == "DEBUG" :
7980 print (f"Connecting Copilot CLI to MCP server on port { port } " )
8081
81- copilot_config = {
82+ mcp_config = {
8283 "mcpServers" : {
8384 "UDB_Server" : {"type" : "sse" , "url" : f"http://localhost:{ port } /sse" , "tools" : ["*" ]}
8485 }
8586 }
8687
88+ # We always need to re-create the MCP config as the dynamically allocated port may change
89+ # between invocations of the tool.
90+ mcp_config_fd , mcp_config_path = mkstemp (prefix = "mcp_config" , suffix = ".json" )
91+ with contextlib .closing (os .fdopen (mcp_config_fd , "w" )) as mcp_config_file :
92+ json .dump (mcp_config , mcp_config_file )
93+
8794 # We run Copilot CLI with a temporary "home directory" so that we can apply a temporary MCP
8895 # configuration and rely on "--resume" finding our previous session automatically.
8996 if not self ._tempdir :
9097 self ._tempdir = mkdtemp (prefix = "udb_explain_copilot_home" )
9198
92- # We always need to re-create the MCP config as the dynamically allocated port may change
93- # between invocations of the tool.
94- config_dir = self ._tempdir / ".copilot"
95- config_dir .mkdir (exist_ok = True )
96- (config_dir / "mcp-config.json" ).write_text (json .dumps (copilot_config ) + "\n " )
97-
9899 result = ""
99100 copilot = None
100101
@@ -105,15 +106,12 @@ async def ask(self, question: str, port: int, tools: list[str]) -> str:
105106 prompt = question
106107
107108 allowed_tools = ["UDB_Server" , "shell(grep)" , "shell(find)" , "shell(cat)" , "shell(xargs)" ]
108- env = {
109- ** os .environ ,
110- "XDG_CONFIG_HOME" : str (self ._tempdir ),
111- "XDG_STATE_HOME" : str (self ._tempdir ),
112- }
113109
114110 try :
115111 copilot = await asyncio .create_subprocess_exec (
116112 str (self .agent_bin ),
113+ "--additional-mcp-config" ,
114+ f"@{ mcp_config_path } " ,
117115 # We can resume unambiguously without specifying a session ID because we're using a
118116 # temporary home directory for the state generated in this session.
119117 * (["--resume" ] if self ._resume else []),
@@ -125,7 +123,11 @@ async def ask(self, question: str, port: int, tools: list[str]) -> str:
125123 "claude-sonnet-4.5" ,
126124 "-p" ,
127125 prompt ,
128- env = env ,
126+ env = {
127+ ** os .environ ,
128+ # Ensure state files are stored in our temporary home, so that resuming works.
129+ "XDG_STATE_HOME" : str (self ._tempdir ),
130+ },
129131 stdin = asyncio .subprocess .PIPE ,
130132 stdout = asyncio .subprocess .PIPE ,
131133 stderr = asyncio .subprocess .PIPE ,
0 commit comments