@@ -2,6 +2,7 @@ import { findProcess } from "./find-process-import.js"
22import { pidToPorts } from "pid-port"
33import { exec } from "child_process"
44import { promisify } from "util"
5+ import { Agent as UndiciAgent } from "undici"
56
67const execAsync = promisify ( exec )
78
@@ -41,6 +42,116 @@ function isWSL(): boolean {
4142 )
4243}
4344
45+ // Cache detected protocol per port to avoid repeated detection
46+ const protocolCache = new Map < number , "http" | "https" > ( )
47+
48+ let insecureHttpsAgent : UndiciAgent | undefined
49+
50+ /**
51+ * Get fetch options for HTTPS requests
52+ * Automatically allows insecure TLS for HTTPS (self-signed certificates)
53+ * Can be disabled via NEXT_DEVTOOLS_ALLOW_INSECURE_TLS=false
54+ */
55+ function getFetchOptions ( protocol : "http" | "https" ) {
56+ // For HTTPS, automatically allow insecure TLS (for self-signed certificates)
57+ // Can be disabled via environment variable: NEXT_DEVTOOLS_ALLOW_INSECURE_TLS=false
58+ const allowInsecure =
59+ protocol === "https" && (
60+ process . env . NEXT_DEVTOOLS_ALLOW_INSECURE_TLS !== "false" ||
61+ process . env . NODE_TLS_REJECT_UNAUTHORIZED === "0"
62+ )
63+
64+ if ( protocol !== "https" || ! allowInsecure ) return { }
65+
66+ if ( ! insecureHttpsAgent ) {
67+ insecureHttpsAgent = new UndiciAgent ( { connect : { rejectUnauthorized : false } } )
68+ }
69+ return { dispatcher : insecureHttpsAgent }
70+ }
71+
72+ /**
73+ * Automatically detect protocol by trying HTTPS first, then falling back to HTTP
74+ * Caches the result per port to avoid repeated detection
75+ */
76+ async function detectProtocol ( port : number ) : Promise < "http" | "https" > {
77+ // Return cached protocol if available
78+ if ( protocolCache . has ( port ) ) {
79+ return protocolCache . get ( port ) !
80+ }
81+
82+ const host = process . env . NEXT_DEVTOOLS_HOST ?? "localhost"
83+
84+ // Try HTTPS first (with insecure TLS allowed for self-signed certificates)
85+ try {
86+ const httpsUrl = `https://${ host } :${ port } /_next/mcp`
87+ const httpsFetchOptions = getFetchOptions ( "https" )
88+ const controller = new AbortController ( )
89+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 500 ) // Short timeout for quick failure
90+
91+ const response = await fetch ( httpsUrl , {
92+ ...httpsFetchOptions ,
93+ method : "POST" ,
94+ headers : {
95+ "Content-Type" : "application/json" ,
96+ Accept : "application/json, text/event-stream" ,
97+ } ,
98+ body : JSON . stringify ( {
99+ jsonrpc : "2.0" ,
100+ method : "tools/list" ,
101+ params : { } ,
102+ id : 1 ,
103+ } ) ,
104+ signal : controller . signal ,
105+ } )
106+
107+ clearTimeout ( timeoutId )
108+
109+ // If HTTPS succeeds (even if it returns an error, it means the protocol is correct)
110+ if ( response . status !== 404 ) {
111+ protocolCache . set ( port , "https" )
112+ return "https"
113+ }
114+ } catch ( error ) {
115+ // HTTPS failed, continue to try HTTP
116+ }
117+
118+ // HTTPS failed, fallback to HTTP
119+ try {
120+ const httpUrl = `http://${ host } :${ port } /_next/mcp`
121+ const controller = new AbortController ( )
122+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 500 )
123+
124+ const response = await fetch ( httpUrl , {
125+ method : "POST" ,
126+ headers : {
127+ "Content-Type" : "application/json" ,
128+ Accept : "application/json, text/event-stream" ,
129+ } ,
130+ body : JSON . stringify ( {
131+ jsonrpc : "2.0" ,
132+ method : "tools/list" ,
133+ params : { } ,
134+ id : 1 ,
135+ } ) ,
136+ signal : controller . signal ,
137+ } )
138+
139+ clearTimeout ( timeoutId )
140+
141+ // HTTP succeeded
142+ if ( response . status !== 404 ) {
143+ protocolCache . set ( port , "http" )
144+ return "http"
145+ }
146+ } catch ( error ) {
147+ // Both protocols failed
148+ }
149+
150+ // Default to HTTP (backward compatibility)
151+ protocolCache . set ( port , "http" )
152+ return "http"
153+ }
154+
44155/**
45156 * Get listening ports for a process using ss command (WSL-compatible)
46157 * ss output format: LISTEN 0 511 *:3000 *:* users:(("next-server",pid=4660,fd=24))
@@ -136,7 +247,10 @@ async function makeNextJsMCPRequest(
136247 method : string ,
137248 params : Record < string , unknown > = { }
138249) : Promise < NextJsMCPResponse > {
139- const url = `http://localhost:${ port } /_next/mcp`
250+ const protocol = await detectProtocol ( port ) // Auto-detect protocol
251+ const host = process . env . NEXT_DEVTOOLS_HOST ?? "localhost"
252+ const url = `${ protocol } ://${ host } :${ port } /_next/mcp`
253+ const fetchOptions = getFetchOptions ( protocol )
140254
141255 const jsonRpcRequest = {
142256 jsonrpc : "2.0" ,
@@ -147,6 +261,7 @@ async function makeNextJsMCPRequest(
147261
148262 try {
149263 const response = await fetch ( url , {
264+ ...fetchOptions ,
150265 method : "POST" ,
151266 headers : {
152267 "Content-Type" : "application/json" ,
@@ -250,11 +365,15 @@ export async function callNextJsTool(
250365 */
251366async function verifyMCPEndpoint ( port : number ) : Promise < boolean > {
252367 try {
253- const url = `http://localhost:${ port } /_next/mcp`
368+ const protocol = await detectProtocol ( port ) // Auto-detect protocol
369+ const host = process . env . NEXT_DEVTOOLS_HOST ?? "localhost"
370+ const url = `${ protocol } ://${ host } :${ port } /_next/mcp`
371+ const fetchOptions = getFetchOptions ( protocol )
254372 const controller = new AbortController ( )
255373 const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 1000 ) // 1 second timeout
256374
257375 const response = await fetch ( url , {
376+ ...fetchOptions ,
258377 method : "POST" ,
259378 headers : {
260379 "Content-Type" : "application/json" ,
@@ -299,3 +418,6 @@ export async function getAllAvailableServers(
299418
300419 return verifiedServers
301420}
421+
422+ // Export detectProtocol for use in nextjs-runtime.ts
423+ export { detectProtocol }
0 commit comments