@@ -30,7 +30,9 @@ import {
3030 IBootstrapService ,
3131 IConfigService ,
3232 IEventBus ,
33+ IHostFileSystem ,
3334 ISessionIndex ,
35+ IWorkspaceInstanceManager ,
3436 ISessionManager ,
3537 ITelemetryService ,
3638 PRINT_MAX_TURNS_DEFAULT ,
@@ -54,8 +56,13 @@ import {
5456 type ISessionScopeHandle ,
5557 type LoopRunResult ,
5658 type PrintBackgroundMode ,
59+ type McpServerConfig ,
5760 type Scope ,
5861} from '@pymodel/agent-core-v2' ;
62+ import {
63+ loadMcpServersDetailed ,
64+ resolveMcpJsonPaths ,
65+ } from '@pymodel/agent-core-v2/app/mcpConfig/configLoader' ;
5966import { createPythinkerDefaultHeaders , createPythinkerDeviceId } from '@pymodel/pythinker-code-oauth' ;
6067import type { GoalUpdated } from '@pymodel/agent-core-v2/features/goal/goalOps' ;
6168import type { TurnEnded } from '@pymodel/agent-core-v2/agent/loop/turnOps' ;
@@ -217,6 +224,13 @@ export async function runV2Print(
217224 ) ;
218225 }
219226
227+ try {
228+ const gated = await listTrustGatedMcpServers ( app , workDir , homeDir ) ;
229+ if ( gated . length > 0 ) stderr . write ( formatTrustGatedMcpWarning ( gated ) ) ;
230+ } catch {
231+ // Best-effort: a broken mcp.json or trust store must not fail the run.
232+ }
233+
220234 const resolved = await resolveNativeSession ( app , opts , workDir , defaultModel , stderr ) ;
221235 restorePermission = resolved . restorePermission ;
222236
@@ -266,6 +280,49 @@ interface ResolvedNativeSession {
266280 readonly goalModel : string | undefined ;
267281}
268282
283+ export interface TrustGatedMcpServer {
284+ readonly name : string ;
285+ readonly target : string ;
286+ }
287+
288+ export async function listTrustGatedMcpServers (
289+ app : Scope ,
290+ workDir : string ,
291+ homeDir : string ,
292+ ) : Promise < readonly TrustGatedMcpServer [ ] > {
293+ const workspace = await app . accessor
294+ . get ( IWorkspaceInstanceManager )
295+ . getOrCreate ( { root : workDir } ) ;
296+ if ( await workspace . program . trust . get ( ) ) return [ ] ;
297+ const fs = app . accessor . get ( IHostFileSystem ) ;
298+ const [ paths , loaded ] = await Promise . all ( [
299+ resolveMcpJsonPaths ( { fs, cwd : workDir , homeDir } ) ,
300+ loadMcpServersDetailed ( { fs, cwd : workDir , homeDir, includeProject : true } ) ,
301+ ] ) ;
302+ const projectPaths = new Set ( [ paths . projectRoot , paths . project ] ) ;
303+ return Object . entries ( loaded . servers )
304+ . filter ( ( [ name ] ) => projectPaths . has ( loaded . origins [ name ] ?? '' ) )
305+ . map ( ( [ name , config ] ) => ( { name, target : describeMcpTarget ( config ) } ) )
306+ . toSorted ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
307+ }
308+
309+ export function formatTrustGatedMcpWarning ( servers : readonly TrustGatedMcpServer [ ] ) : string {
310+ const noun = servers . length === 1 ? 'server' : 'servers' ;
311+ const list = servers . map ( ( server ) => `${ server . name } (${ server . target } )` ) . join ( ', ' ) ;
312+ return (
313+ `Warning: this folder is not trusted; skipped ${ servers . length } project-level MCP ${ noun } : ${ list } .\n` +
314+ ' Run `pythinker` here and choose "Trust this folder" to enable them.\n\n'
315+ ) ;
316+ }
317+
318+ function describeMcpTarget ( config : McpServerConfig ) : string {
319+ if ( config . transport === 'stdio' ) {
320+ const args = config . args === undefined ? '' : ` ${ config . args . join ( ' ' ) } ` ;
321+ return `stdio: ${ config . command } ${ args } ` ;
322+ }
323+ return `${ config . transport } : ${ config . url } ` ;
324+ }
325+
269326async function resolveNativeSession (
270327 app : Scope ,
271328 opts : CLIOptions ,
0 commit comments