@@ -647,7 +647,7 @@ describe("managed install end-to-end MCP", { timeout: 120_000 }, async () => {
647647
648648 // Verify the binary is executable
649649 test ( "managed install produces a runnable binary" , async ( ) => {
650- const { stdout, stderr } = await execFileAsync ( binaryPath , [ "--version" ] , { timeout : 5000 } ) ;
650+ const { stdout, stderr } = await execFileAsync ( binaryPath , [ "--version" ] , { timeout : 15000 } ) ;
651651 const output = `${ stdout } ${ stderr } ` . trim ( ) ;
652652 const version = parsePatchloomVersion ( output ) ;
653653 assert . ok ( version , `should parse version from managed binary: ${ output } ` ) ;
@@ -757,6 +757,70 @@ describe("managed install end-to-end MCP", { timeout: 120_000 }, async () => {
757757 }
758758 } ) ;
759759
760+ test ( "MCP tools/call modifies a file on disk" , async ( ) => {
761+ // Create a temp directory with a JSON file to edit via MCP.
762+ // The MCP server resolves paths relative to its cwd, so we
763+ // spawn the server inside the temp directory and use a relative path.
764+ const workDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "patchloom-mcp-call-" ) ) ;
765+ await fs . writeFile ( path . join ( workDir , "config.json" ) , '{"port": 3000}\n' , "utf8" ) ;
766+
767+ const child = execFile ( binaryPath , [ "mcp-server" ] , { timeout : 15000 , cwd : workDir } ) ;
768+ let stdout = "" ;
769+ child . stdout ! . on ( "data" , ( data : Buffer ) => { stdout += data . toString ( ) ; } ) ;
770+
771+ // Initialize
772+ child . stdin ! . write ( JSON . stringify ( {
773+ jsonrpc : "2.0" , id : 1 , method : "initialize" ,
774+ params : {
775+ protocolVersion : "2024-11-05" , capabilities : { } ,
776+ clientInfo : { name : "e2e-test" , version : "0.0.1" }
777+ }
778+ } ) + "\n" ) ;
779+
780+ let deadline = Date . now ( ) + 10000 ;
781+ while ( ! stdout . includes ( '"id":1' ) && Date . now ( ) < deadline ) {
782+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
783+ }
784+
785+ child . stdin ! . write ( JSON . stringify ( {
786+ jsonrpc : "2.0" , method : "notifications/initialized"
787+ } ) + "\n" ) ;
788+
789+ // Call doc_set to change port from 3000 to 8080 (relative path)
790+ child . stdin ! . write ( JSON . stringify ( {
791+ jsonrpc : "2.0" , id : 3 , method : "tools/call" ,
792+ params : {
793+ name : "doc_set" ,
794+ arguments : { path : "config.json" , selector : "port" , value : 8080 }
795+ }
796+ } ) + "\n" ) ;
797+
798+ deadline = Date . now ( ) + 10000 ;
799+ while ( ! stdout . includes ( '"id":3' ) && Date . now ( ) < deadline ) {
800+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
801+ }
802+
803+ child . kill ( ) ;
804+
805+ // Verify the tool call succeeded
806+ const lines = stdout . trim ( ) . split ( "\n" ) ;
807+ const callLine = lines . find ( ( line ) => line . includes ( '"id":3' ) ) ;
808+ assert . ok ( callLine , "should have a tools/call response" ) ;
809+ const callResponse = JSON . parse ( callLine ) as Record < string , unknown > ;
810+ assert . equal ( callResponse . jsonrpc , "2.0" ) ;
811+ assert . equal ( callResponse . id , 3 ) ;
812+ const callResult = callResponse . result as Record < string , unknown > ;
813+ assert . ok ( callResult , "tools/call should return a result (not an error)" ) ;
814+ assert . ok ( ! callResult . isError ,
815+ `tools/call should not be an error: ${ JSON . stringify ( callResult ) } ` ) ;
816+
817+ // Verify the file was actually modified on disk
818+ const content = JSON . parse ( await fs . readFile ( path . join ( workDir , "config.json" ) , "utf8" ) ) as Record < string , unknown > ;
819+ assert . equal ( content . port , 8080 , "doc_set should have changed port to 8080" ) ;
820+
821+ await fs . rm ( workDir , { recursive : true , force : true } ) ;
822+ } ) ;
823+
760824 // Cleanup
761825 test ( "cleanup managed install temp directory" , async ( ) => {
762826 await fs . rm ( installDir , { recursive : true , force : true } ) ;
0 commit comments