@@ -127,6 +127,7 @@ const URL_ELICITATION_FIELD_KEY = "elicitation"
127127
128128export interface Interface {
129129 readonly servers : ( ) => Effect . Effect < ServerInfo [ ] >
130+ readonly add : ( server : ServerName | string , config : typeof ConfigMCP . Server . Type ) => Effect . Effect < void >
130131 readonly connect : ( server : ServerName | string ) => Effect . Effect < void , NotFoundError >
131132 readonly disconnect : ( server : ServerName | string ) => Effect . Effect < void , NotFoundError >
132133 readonly tools : ( ) => Effect . Effect < Tool [ ] >
@@ -185,14 +186,9 @@ export const layer = Layer.effect(
185186
186187 // Register every remote server as an OAuth integration so credentials live in the global store
187188 // rather than in committed config. Servers that connect anonymously simply never use the method.
188- const registrations : Array < {
189- readonly name : ServerName
190- readonly remote : typeof ConfigMCP . Remote . Type
191- readonly integrationID : Integration . ID
192- readonly methodID : Integration . MethodID
193- } > = [ ]
194- for ( const [ name , entry ] of runtime ) {
195- if ( entry . config . type !== "remote" || entry . config . oauth === false ) continue
189+ const owned = new Set < Integration . ID > ( )
190+ const register = Effect . fnUntraced ( function * ( name : ServerName , entry : ServerEntry ) {
191+ if ( entry . config . type !== "remote" || entry . config . oauth === false ) return
196192 const remote = entry . config
197193 // Key identity on name + url, not url alone: two configs for the same url under different names are
198194 // distinct logical servers that may hold different accounts, so they must not share a credential row.
@@ -202,27 +198,24 @@ export const layer = Layer.effect(
202198 . update ( name + "\u0000" + remote . url )
203199 . digest ( "hex" )
204200 . slice ( 0 , 16 )
205- entry . integrationID = Integration . ID . make ( suffix )
206- registrations . push ( {
207- name,
208- remote,
209- integrationID : entry . integrationID ,
210- methodID : Integration . MethodID . make ( suffix ) ,
211- } )
212- }
213- if ( registrations . length > 0 )
214- yield * integration . transform ( ( draft ) => {
215- for ( const reg of registrations ) {
216- draft . update ( reg . integrationID , ( ref ) => {
217- ref . name = reg . name
201+ const integrationID = Integration . ID . make ( suffix )
202+ entry . integrationID = integrationID
203+ owned . add ( integrationID )
204+ const methodID = Integration . MethodID . make ( suffix )
205+ yield * integration
206+ . transform ( ( draft ) => {
207+ draft . update ( integrationID , ( ref ) => {
208+ ref . name = name
218209 } )
219210 draft . method . update ( {
220- integrationID : reg . integrationID ,
221- method : { id : reg . methodID , type : "oauth" , label : reg . name } ,
222- authorize : ( ) => MCPOAuth . authorize ( { name : reg . name , config : reg . remote , methodID : reg . methodID } ) ,
211+ integrationID,
212+ method : { id : methodID , type : "oauth" , label : name } ,
213+ authorize : ( ) => MCPOAuth . authorize ( { name, config : remote , methodID } ) ,
223214 } )
224- }
225- } )
215+ } )
216+ . pipe ( Scope . provide ( root ) )
217+ } )
218+ yield * Effect . forEach ( runtime , ( [ name , entry ] ) => register ( name , entry ) , { discard : true } )
226219
227220 const requireServer = Effect . fnUntraced ( function * ( server : ServerName | string ) {
228221 const name = ServerName . make ( server )
@@ -511,6 +504,18 @@ export const layer = Layer.effect(
511504 yield * events . publish ( McpEvent . StatusChanged , { server : name } ) . pipe ( Effect . ignore )
512505 } ) . pipe ( Effect . ensuring ( Deferred . succeed ( entry . startup , undefined ) ) )
513506
507+ const stopServer = Effect . fnUntraced ( function * ( name : ServerName , entry : ServerEntry ) {
508+ const scope = entry . scope
509+ entry . scope = undefined
510+ entry . client = undefined
511+ entry . tools = undefined
512+ entry . prompts = undefined
513+ if ( scope ) yield * Scope . close ( scope , Exit . void )
514+ yield * events . publish ( McpEvent . ToolsChanged , { server : name } ) . pipe ( Effect . ignore )
515+ yield * events . publish ( McpEvent . ResourcesChanged , { server : name } ) . pipe ( Effect . ignore )
516+ yield * events . publish ( Command . Event . Updated , { } ) . pipe ( Effect . ignore )
517+ } )
518+
514519 // Disabled servers settle their startup immediately so queries never block on them.
515520 for ( const [ name , entry ] of runtime ) {
516521 if ( entry . config . disabled ) {
@@ -523,21 +528,13 @@ export const layer = Layer.effect(
523528
524529 // Bring a server online (or back to needs_auth) when its integration's credential changes, so an
525530 // OAuth login takes effect without a restart. Only fires for the integrations we registered.
526- const owned = new Set ( registrations . map ( ( reg ) => reg . integrationID ) )
527531 const reconnect = ( integrationID : Integration . ID ) =>
528532 Effect . gen ( function * ( ) {
529533 const match = Array . from ( runtime ) . find ( ( [ , entry ] ) => entry . integrationID === integrationID )
530534 if ( ! match ) return
531535 const [ name , entry ] = match
532536 if ( entry . status . status === "disabled" ) return
533- if ( entry . scope ) {
534- yield * Scope . close ( entry . scope , Exit . void )
535- entry . scope = undefined
536- entry . client = undefined
537- entry . tools = undefined
538- entry . prompts = undefined
539- yield * events . publish ( Command . Event . Updated , { } ) . pipe ( Effect . ignore )
540- }
537+ if ( entry . scope ) yield * stopServer ( name , entry )
541538 yield * startServer ( name , entry )
542539 } )
543540 fork (
@@ -564,33 +561,41 @@ export const layer = Layer.effect(
564561 } ) ,
565562 )
566563 } ) ,
564+ add : Effect . fn ( "MCP.add" ) ( function * ( server , config ) {
565+ const name = ServerName . make ( server )
566+ const previous = runtime . get ( name )
567+ if ( previous ) {
568+ yield * stopServer ( name , previous )
569+ if ( previous . integrationID ) {
570+ const integrationID = previous . integrationID
571+ owned . delete ( integrationID )
572+ yield * integration . transform ( ( draft ) => draft . remove ( integrationID ) ) . pipe ( Scope . provide ( root ) )
573+ }
574+ }
575+ const entry : ServerEntry = {
576+ config : { ...config , timeout : { ...timeout , ...config . timeout } } ,
577+ status : { status : "pending" } ,
578+ startup : Deferred . makeUnsafe < void > ( ) ,
579+ }
580+ runtime . set ( name , entry )
581+ yield * register ( name , entry )
582+ if ( config . disabled ) {
583+ entry . status = { status : "disabled" }
584+ Deferred . doneUnsafe ( entry . startup , Exit . void )
585+ yield * events . publish ( McpEvent . StatusChanged , { server : name } ) . pipe ( Effect . ignore )
586+ return
587+ }
588+ yield * startServer ( name , entry )
589+ } ) ,
567590 connect : Effect . fn ( "MCP.connect" ) ( function * ( server ) {
568591 const target = yield * requireServer ( server )
569- if ( target . entry . scope ) {
570- const scope = target . entry . scope
571- target . entry . scope = undefined
572- target . entry . client = undefined
573- target . entry . tools = undefined
574- target . entry . prompts = undefined
575- yield * Scope . close ( scope , Exit . void )
576- yield * events . publish ( McpEvent . ToolsChanged , { server : target . name } ) . pipe ( Effect . ignore )
577- yield * events . publish ( McpEvent . ResourcesChanged , { server : target . name } ) . pipe ( Effect . ignore )
578- yield * events . publish ( Command . Event . Updated , { } ) . pipe ( Effect . ignore )
579- }
592+ if ( target . entry . scope ) yield * stopServer ( target . name , target . entry )
580593 yield * startServer ( target . name , target . entry )
581594 } ) ,
582595 disconnect : Effect . fn ( "MCP.disconnect" ) ( function * ( server ) {
583596 const target = yield * requireServer ( server )
584- const scope = target . entry . scope
585- target . entry . scope = undefined
586- target . entry . client = undefined
587- target . entry . tools = undefined
588- target . entry . prompts = undefined
589- if ( scope ) yield * Scope . close ( scope , Exit . void )
597+ yield * stopServer ( target . name , target . entry )
590598 target . entry . status = { status : "disabled" }
591- yield * events . publish ( McpEvent . ToolsChanged , { server : target . name } ) . pipe ( Effect . ignore )
592- yield * events . publish ( McpEvent . ResourcesChanged , { server : target . name } ) . pipe ( Effect . ignore )
593- yield * events . publish ( Command . Event . Updated , { } ) . pipe ( Effect . ignore )
594599 yield * events . publish ( McpEvent . StatusChanged , { server : target . name } ) . pipe ( Effect . ignore )
595600 } ) ,
596601 tools : Effect . fn ( "MCP.tools" ) ( function * ( ) {
0 commit comments