1+ import process from "node:process" ;
12import { Client } from "@modelcontextprotocol/sdk/client/index.js" ;
23import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" ;
4+ import type { AppConfig , McpServerConfig } from "../config.js" ;
35
4- function normalizeToolContent ( result ) {
6+ interface ToolTextItem {
7+ text ?: string ;
8+ json ?: unknown ;
9+ }
10+
11+ interface ToolResultLike {
12+ content ?: string | ToolTextItem [ ] ;
13+ }
14+
15+ interface McpConnection {
16+ client : {
17+ connect : ( transport : unknown ) => Promise < void > ;
18+ listTools : ( ) => Promise < { tools ?: Array < { name ?: string } > } > ;
19+ callTool : ( input : {
20+ name : string ;
21+ arguments : Record < string , unknown > ;
22+ } ) => Promise < unknown > ;
23+ } ;
24+ transport : {
25+ close ?: ( ) => Promise < void > ;
26+ } ;
27+ }
28+
29+ export interface McpServerStatus {
30+ name : string ;
31+ command : string ;
32+ args : string [ ] ;
33+ cwd : string ;
34+ enabled : boolean ;
35+ connected : boolean ;
36+ }
37+
38+ export interface McpClientSnapshot {
39+ disabledServers : string [ ] ;
40+ }
41+
42+ interface McpClientOptions {
43+ onChange ?: ( snapshot : McpClientSnapshot ) => void ;
44+ }
45+
46+ function normalizeToolContent ( result : unknown ) : string {
547 if ( ! result ) return "" ;
648
749 if ( typeof result === "string" ) return result ;
850
9- if ( Array . isArray ( result . content ) ) {
10- return result . content
51+ const content = ( result as ToolResultLike & { content ?: unknown } ) . content ;
52+
53+ if ( Array . isArray ( content ) ) {
54+ return content
1155 . map ( ( item ) => {
1256 if ( typeof item === "string" ) return item ;
1357 if ( item ?. text ) return item . text ;
@@ -18,44 +62,60 @@ function normalizeToolContent(result) {
1862 . join ( "\n" ) ;
1963 }
2064
21- if ( result ?. content ?. text ) return String ( result . content . text ) ;
22- if ( result ?. content ?. json ) return JSON . stringify ( result . content . json ) ;
65+ if ( content && typeof content === "object" ) {
66+ const record = content as ToolTextItem ;
67+ if ( record . text ) {
68+ return String ( record . text ) ;
69+ }
70+
71+ if ( record . json !== undefined ) {
72+ return JSON . stringify ( record . json ) ;
73+ }
74+ }
2375
2476 return JSON . stringify ( result ) ;
2577}
2678
2779export class McpClient {
28- constructor ( config , { onChange } = { } ) {
80+ readonly config : Pick < AppConfig , "mcp" > ;
81+ readonly connections : Map < string , McpConnection > ;
82+ disabledServers : Set < string > ;
83+ private readonly onChange ?: ( snapshot : McpClientSnapshot ) => void ;
84+
85+ constructor (
86+ config : Pick < AppConfig , "mcp" > ,
87+ { onChange } : McpClientOptions = { }
88+ ) {
2989 this . config = config ;
3090 this . connections = new Map ( ) ;
3191 this . disabledServers = new Set ( ) ;
3292 this . onChange = onChange ;
3393 }
3494
35- hasServers ( ) {
95+ hasServers ( ) : boolean {
3696 return this . config . mcp . servers . length > 0 ;
3797 }
3898
39- getServerConfig ( serverName ) {
99+ getServerConfig ( serverName : string ) : McpServerConfig | null {
40100 return (
41101 this . config . mcp . servers . find ( ( server ) => server . name === serverName ) ||
42102 null
43103 ) ;
44104 }
45105
46- hasServer ( serverName ) {
106+ hasServer ( serverName : string ) : boolean {
47107 return Boolean ( this . getServerConfig ( serverName ) ) ;
48108 }
49109
50- isServerEnabled ( serverName ) {
110+ isServerEnabled ( serverName : string ) : boolean {
51111 return this . hasServer ( serverName ) && ! this . disabledServers . has ( serverName ) ;
52112 }
53113
54- isServerConnected ( serverName ) {
114+ isServerConnected ( serverName : string ) : boolean {
55115 return this . connections . has ( serverName ) ;
56116 }
57117
58- listServers ( ) {
118+ listServers ( ) : McpServerStatus [ ] {
59119 return this . config . mcp . servers . map ( ( server ) => ( {
60120 name : server . name ,
61121 command : server . command ,
@@ -66,27 +126,31 @@ export class McpClient {
66126 } ) ) ;
67127 }
68128
69- async connectAll ( ) {
129+ async connectAll ( ) : Promise < void > {
70130 for ( const server of this . config . mcp . servers ) {
71131 await this . connectServer ( server ) ;
72132 }
73133 }
74134
75- async connectServer ( server ) {
135+ async connectServer ( server : McpServerConfig ) : Promise < void > {
76136 if ( this . disabledServers . has ( server . name ) ) {
77137 return ;
78138 }
79139
80140 if ( this . connections . has ( server . name ) ) return ;
81141
142+ const env = Object . fromEntries (
143+ Object . entries ( {
144+ ...process . env ,
145+ ...server . env
146+ } ) . filter ( ( [ , value ] ) => value !== undefined )
147+ ) as Record < string , string > ;
148+
82149 const transport = new StdioClientTransport ( {
83150 command : server . command ,
84151 args : server . args ,
85152 cwd : server . cwd ,
86- env : {
87- ...process . env ,
88- ...server . env
89- }
153+ env
90154 } ) ;
91155
92156 const client = new Client (
@@ -97,13 +161,13 @@ export class McpClient {
97161 {
98162 capabilities : { }
99163 }
100- ) ;
164+ ) as McpConnection [ "client" ] ;
101165
102166 await client . connect ( transport ) ;
103167 this . connections . set ( server . name , { client, transport } ) ;
104168 }
105169
106- async connectServerByName ( serverName ) {
170+ async connectServerByName ( serverName : string ) : Promise < void > {
107171 const server = this . getServerConfig ( serverName ) ;
108172 if ( ! server ) {
109173 throw new Error ( `Unknown MCP server: ${ serverName } ` ) ;
@@ -116,7 +180,7 @@ export class McpClient {
116180 await this . connectServer ( server ) ;
117181 }
118182
119- async disconnectServer ( serverName ) {
183+ async disconnectServer ( serverName : string ) : Promise < boolean > {
120184 const conn = this . connections . get ( serverName ) ;
121185 if ( ! conn ) return false ;
122186
@@ -130,7 +194,7 @@ export class McpClient {
130194 return true ;
131195 }
132196
133- async reconnectServer ( serverName ) {
197+ async reconnectServer ( serverName : string ) : Promise < McpServerStatus | null > {
134198 if ( ! this . hasServer ( serverName ) ) {
135199 throw new Error ( `Unknown MCP server: ${ serverName } ` ) ;
136200 }
@@ -146,7 +210,9 @@ export class McpClient {
146210 ) ;
147211 }
148212
149- async disableServer ( serverName ) {
213+ async disableServer (
214+ serverName : string
215+ ) : Promise < ( McpServerStatus & { changed : boolean } ) | null > {
150216 if ( ! this . hasServer ( serverName ) ) {
151217 throw new Error ( `Unknown MCP server: ${ serverName } ` ) ;
152218 }
@@ -165,7 +231,9 @@ export class McpClient {
165231 return current ? { ...current , changed : true } : null ;
166232 }
167233
168- async enableServer ( serverName ) {
234+ async enableServer (
235+ serverName : string
236+ ) : Promise < ( McpServerStatus & { changed : boolean } ) | null > {
169237 if ( ! this . hasServer ( serverName ) ) {
170238 throw new Error ( `Unknown MCP server: ${ serverName } ` ) ;
171239 }
@@ -188,13 +256,13 @@ export class McpClient {
188256 return current ? { ...current , changed } : null ;
189257 }
190258
191- exportState ( ) {
259+ exportState ( ) : McpClientSnapshot {
192260 return {
193261 disabledServers : [ ...this . disabledServers ] . sort ( )
194262 } ;
195263 }
196264
197- restoreState ( snapshot = { } ) {
265+ restoreState ( snapshot : Partial < McpClientSnapshot > = { } ) : void {
198266 const disabledServers = Array . isArray ( snapshot ?. disabledServers )
199267 ? snapshot . disabledServers . filter ( ( serverName ) =>
200268 this . hasServer ( serverName )
@@ -204,14 +272,22 @@ export class McpClient {
204272 this . disabledServers = new Set ( disabledServers ) ;
205273 }
206274
207- async listTools ( serverName ) {
275+ async listTools ( serverName : string ) : Promise < Array < { name ?: string } > > {
208276 const conn = this . connections . get ( serverName ) ;
209277 if ( ! conn ) throw new Error ( `MCP server not connected: ${ serverName } ` ) ;
210278 const res = await conn . client . listTools ( ) ;
211279 return res . tools || [ ] ;
212280 }
213281
214- async callTool ( { serverName, toolName, args = { } } ) {
282+ async callTool ( {
283+ serverName,
284+ toolName,
285+ args = { }
286+ } : {
287+ serverName : string ;
288+ toolName : string ;
289+ args ?: Record < string , unknown > ;
290+ } ) : Promise < string > {
215291 const conn = this . connections . get ( serverName ) ;
216292 if ( ! conn ) throw new Error ( `MCP server not connected: ${ serverName } ` ) ;
217293
@@ -223,12 +299,12 @@ export class McpClient {
223299 return normalizeToolContent ( result ) ;
224300 }
225301
226- async gatherContextForTask ( taskText ) {
302+ async gatherContextForTask ( taskText : string ) : Promise < string > {
227303 if ( ! this . connections . size || ! taskText . trim ( ) ) {
228304 return "" ;
229305 }
230306
231- const contextBlocks = [ ] ;
307+ const contextBlocks : string [ ] = [ ] ;
232308 const toolNameHints = [
233309 "search" ,
234310 "query" ,
@@ -249,7 +325,7 @@ export class McpClient {
249325 return toolNameHints . some ( ( hint ) => name . includes ( hint ) ) ;
250326 } ) ;
251327
252- if ( ! preferredTool ) continue ;
328+ if ( ! preferredTool ?. name ) continue ;
253329
254330 const result = await conn . client . callTool ( {
255331 name : preferredTool . name ,
@@ -265,19 +341,18 @@ export class McpClient {
265341
266342 contextBlocks . push ( `[${ serverName } /${ preferredTool . name } ]\n${ text } ` ) ;
267343 } catch ( error ) {
268- contextBlocks . push (
269- `[${ serverName } ] MCP query failed: ${ error . message } `
270- ) ;
344+ const message = error instanceof Error ? error . message : String ( error ) ;
345+ contextBlocks . push ( `[${ serverName } ] MCP query failed: ${ message } ` ) ;
271346 }
272347 }
273348
274349 return contextBlocks . join ( "\n\n" ) ;
275350 }
276351
277- async closeAll ( ) {
352+ async closeAll ( ) : Promise < void > {
278353 for ( const { transport } of this . connections . values ( ) ) {
279354 try {
280- await transport . close ( ) ;
355+ await transport . close ?. ( ) ;
281356 } catch {
282357 // Ignore close errors on shutdown.
283358 }
0 commit comments