@@ -7,6 +7,7 @@ import { type } from "arktype";
77import { getLogger } from "@intx/log" ;
88import type { MCPServerConfig } from "../config/settings.js" ;
99import { isBuiltinExaMCPServer } from "../mcp/exa.js" ;
10+ import { isHttpServer } from "../mcp/is-http-server.js" ;
1011import { LOG_NAMESPACE_ROOT , SETTINGS_DIR_NAME } from "../branding.js" ;
1112
1213const logger = getLogger ( [ LOG_NAMESPACE_ROOT , "trust" ] ) ;
@@ -298,23 +299,23 @@ export function mcpServerFingerprint(server: MCPServerConfig): string {
298299 return createHash ( "sha256" ) . update ( payload ) . digest ( "hex" ) ;
299300}
300301
301- // Display-only argv quoting for the MCP trust prompt: an arg containing
302- // whitespace (or a quote, or empty) renders double-quoted so ["a b"] and
303- // ["a", "b"] never look alike. Control characters render as visible escape
304- // sequences (\n, \r, \t, \xNN) so a newline-bearing arg cannot spoof extra
305- // prompt lines — output is always single-line per token. Approval identity
306- // still comes from mcpServerFingerprint above, never from this rendering.
302+ // Display-only quoting for the MCP trust prompt: argv, name, and url all pass
303+ // through the same escape so a newline, quote, or Unicode/C1 line break cannot
304+ // spoof extra prompt lines. An arg containing whitespace (or a quote, or empty)
305+ // renders double-quoted so ["a b"] and ["a", "b"] never look alike. Approval
306+ // identity still comes from mcpServerFingerprint above, never from this rendering.
307307function isTrustPromptControlChar ( code : number ) : boolean {
308- return code <= 0x1f || code === 0x7f ;
308+ return (
309+ code <= 0x1f ||
310+ code === 0x7f ||
311+ ( code >= 0x80 && code <= 0x9f ) ||
312+ code === 0x2028 ||
313+ code === 0x2029
314+ ) ;
309315}
310316
311- function quoteMcpTrustArg ( arg : string ) : string {
312- const needsQuotes =
313- arg === "" ||
314- / [ \s " ] / . test ( arg ) ||
315- [ ...arg ] . some ( ( ch ) => isTrustPromptControlChar ( ch . charCodeAt ( 0 ) ) ) ;
316- if ( ! needsQuotes ) return arg ;
317- const named = arg
317+ function escapeMcpTrustText ( value : string ) : string {
318+ const named = value
318319 . replace ( / \\ / g, "\\\\" )
319320 . replace ( / " / g, '\\"' )
320321 . replace ( / \n / g, "\\n" )
@@ -323,11 +324,25 @@ function quoteMcpTrustArg(arg: string): string {
323324 let escaped = "" ;
324325 for ( const ch of named ) {
325326 const code = ch . charCodeAt ( 0 ) ;
326- escaped += isTrustPromptControlChar ( code )
327- ? `\\x${ code . toString ( 16 ) . toUpperCase ( ) . padStart ( 2 , "0" ) } `
328- : ch ;
327+ if ( ! isTrustPromptControlChar ( code ) ) {
328+ escaped += ch ;
329+ continue ;
330+ }
331+ escaped +=
332+ code <= 0xff
333+ ? `\\x${ code . toString ( 16 ) . toUpperCase ( ) . padStart ( 2 , "0" ) } `
334+ : `\\u${ code . toString ( 16 ) . toUpperCase ( ) . padStart ( 4 , "0" ) } ` ;
329335 }
330- return `"${ escaped } "` ;
336+ return escaped ;
337+ }
338+
339+ function quoteMcpTrustArg ( arg : string ) : string {
340+ const needsQuotes =
341+ arg === "" ||
342+ / [ \s " ] / . test ( arg ) ||
343+ [ ...arg ] . some ( ( ch ) => isTrustPromptControlChar ( ch . charCodeAt ( 0 ) ) ) ;
344+ if ( ! needsQuotes ) return arg ;
345+ return `"${ escapeMcpTrustText ( arg ) } "` ;
331346}
332347
333348function formatMcpSpawnCommand ( command : string , args : string [ ] ) : string {
@@ -338,14 +353,16 @@ function formatMcpSpawnCommand(command: string, args: string[]): string {
338353}
339354
340355export function formatMcpTrustQuestion ( server : MCPServerConfig ) : string {
341- return (
342- `Trust local MCP server "${ server . name } " for this project?` +
343- ( server . command !== undefined
344- ? `\nCommand: ${ formatMcpSpawnCommand ( server . command , server . args ?? [ ] ) } `
345- : server . url !== undefined
346- ? `\nURL: ${ server . url } `
347- : "" )
348- ) ;
356+ const header = `Trust local MCP server "${ escapeMcpTrustText ( server . name ) } " for this project?` ;
357+ if ( isHttpServer ( server ) ) {
358+ return server . url !== undefined
359+ ? `${ header } \nURL: ${ quoteMcpTrustArg ( server . url ) } `
360+ : header ;
361+ }
362+ if ( server . command !== undefined ) {
363+ return `${ header } \nCommand: ${ formatMcpSpawnCommand ( server . command , server . args ?? [ ] ) } ` ;
364+ }
365+ return header ;
349366}
350367
351368export function isMcpServerTrusted (
0 commit comments