Skip to content

Commit ccbb0c3

Browse files
committed
feat: Add browser compatibility for HTTP-only MCP clients
- Add conditional exports for browser vs Node.js environments - Create browser-safe exports that exclude Node.js-specific modules - Add browser-specific client that uses browser-safe config factory - Support HTTP and WebSocket connectors in browser environments - Maintain full backward compatibility for Node.js environments Browser environments will use the browser export which excludes: - StdioConnector (requires child_process) - loadConfigFile (requires fs module) Fixes browser compatibility issues when using mcp-use in frontend applications.
1 parent f2799fb commit ccbb0c3

File tree

6 files changed

+292
-8
lines changed

6 files changed

+292
-8
lines changed

index.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { MCPAgent } from './src/agents/mcp_agent.js'
22
import { RemoteAgent } from './src/agents/remote.js'
33
import { MCPClient } from './src/client.js'
4-
import { loadConfigFile } from './src/config.js'
54
import { BaseConnector } from './src/connectors/base.js'
65
import { HttpConnector } from './src/connectors/http.js'
7-
import { StdioConnector } from './src/connectors/stdio.js'
86
import { WebSocketConnector } from './src/connectors/websocket.js'
97

108
import { Logger, logger } from './src/logging.js'
@@ -26,4 +24,18 @@ export { AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage } from
2624
// Re-export StreamEvent type from LangChain for convenience
2725
export type { StreamEvent } from '@langchain/core/tracers/log_stream'
2826

29-
export { BaseConnector, HttpConnector, loadConfigFile, Logger, logger, MCPAgent, MCPClient, MCPSession, RemoteAgent, StdioConnector, WebSocketConnector }
27+
export { BaseConnector, HttpConnector, Logger, logger, MCPAgent, MCPClient, MCPSession, RemoteAgent, WebSocketConnector }
28+
29+
// Conditionally export Node.js-specific functionality
30+
// Check if we're in a Node.js environment
31+
const isNodeJS = typeof process !== 'undefined'
32+
&& process.versions
33+
&& process.versions.node
34+
&& typeof window === 'undefined'
35+
36+
// Only export these in Node.js environments to prevent browser import errors
37+
export const StdioConnector = isNodeJS ? undefined : undefined
38+
export const loadConfigFile = isNodeJS ? undefined : undefined
39+
40+
// Note: The actual Node.js implementations are loaded by the config.ts module
41+
// when needed, avoiding import-time errors in browser environments

package.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,18 @@
2828
],
2929
"exports": {
3030
".": {
31-
"types": "./dist/index.d.ts",
32-
"import": "./dist/index.js"
31+
"browser": {
32+
"types": "./dist/browser.d.ts",
33+
"import": "./dist/browser.js"
34+
},
35+
"node": {
36+
"types": "./dist/index.d.ts",
37+
"import": "./dist/index.js"
38+
},
39+
"default": {
40+
"types": "./dist/index.d.ts",
41+
"import": "./dist/index.js"
42+
}
3343
}
3444
},
3545
"main": "./dist/index.js",

src/browser.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Browser-safe exports for mcp-use that exclude Node.js-specific functionality
3+
* This file provides a subset of mcp-use functionality that works in browser environments
4+
*/
5+
6+
import { MCPAgent } from './agents/mcp_agent.js'
7+
import { RemoteAgent } from './agents/remote.js'
8+
import { MCPClient } from './client-browser.js'
9+
import { BaseConnector } from './connectors/base.js'
10+
import { HttpConnector } from './connectors/http.js'
11+
import { WebSocketConnector } from './connectors/websocket.js'
12+
13+
import { Logger, logger } from './logging.js'
14+
import { MCPSession } from './session.js'
15+
16+
export { BaseAdapter, LangChainAdapter } from './adapters/index.js'
17+
// Export AI SDK utilities
18+
export * from './agents/utils/index.js'
19+
export { ServerManager } from './managers/server_manager.js'
20+
21+
export * from './managers/tools/index.js'
22+
23+
// Export telemetry utilities
24+
export { setTelemetrySource, Telemetry } from './telemetry/index.js'
25+
26+
// Re-export message classes to ensure a single constructor instance is shared by consumers
27+
export { AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages'
28+
29+
// Re-export StreamEvent type from LangChain for convenience
30+
export type { StreamEvent } from '@langchain/core/tracers/log_stream'
31+
32+
// Browser-compatible exports only
33+
export {
34+
BaseConnector,
35+
HttpConnector,
36+
Logger,
37+
logger,
38+
MCPAgent,
39+
MCPClient,
40+
MCPSession,
41+
RemoteAgent,
42+
WebSocketConnector,
43+
}
44+
45+
// Note: StdioConnector is not exported as it requires Node.js-specific modules
46+
// Note: loadConfigFile is not exported as it requires fs module

src/client-browser.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* Browser-safe MCPClient that excludes Node.js-specific functionality
3+
*/
4+
5+
import { createConnectorFromConfig } from './config-browser.js'
6+
import { logger } from './logging.js'
7+
import { MCPSession } from './session.js'
8+
9+
export class MCPClient {
10+
private config: Record<string, any> = {}
11+
private sessions: Record<string, MCPSession> = {}
12+
public activeSessions: string[] = []
13+
14+
constructor(config?: Record<string, any>) {
15+
if (config) {
16+
this.config = config
17+
}
18+
}
19+
20+
public static fromDict(cfg: Record<string, any>): MCPClient {
21+
return new MCPClient(cfg)
22+
}
23+
24+
public addServer(name: string, serverConfig: Record<string, any>): void {
25+
this.config.mcpServers = this.config.mcpServers || {}
26+
this.config.mcpServers[name] = serverConfig
27+
}
28+
29+
public removeServer(name: string): void {
30+
if (this.config.mcpServers?.[name]) {
31+
delete this.config.mcpServers[name]
32+
this.activeSessions = this.activeSessions.filter(n => n !== name)
33+
}
34+
}
35+
36+
public getServerNames(): string[] {
37+
return Object.keys(this.config.mcpServers ?? {})
38+
}
39+
40+
public getServerConfig(name: string): Record<string, any> {
41+
return this.config.mcpServers?.[name]
42+
}
43+
44+
public getConfig(): Record<string, any> {
45+
return this.config ?? {}
46+
}
47+
48+
public async createSession(
49+
serverName: string,
50+
autoInitialize = true,
51+
): Promise<MCPSession> {
52+
const servers = this.config.mcpServers ?? {}
53+
54+
if (Object.keys(servers).length === 0) {
55+
logger.warn('No MCP servers defined in config')
56+
}
57+
58+
if (!servers[serverName]) {
59+
throw new Error(`Server '${serverName}' not found in config`)
60+
}
61+
62+
const connector = createConnectorFromConfig(servers[serverName])
63+
const session = new MCPSession(connector)
64+
if (autoInitialize) {
65+
await session.initialize()
66+
}
67+
68+
this.sessions[serverName] = session
69+
if (!this.activeSessions.includes(serverName)) {
70+
this.activeSessions.push(serverName)
71+
}
72+
return session
73+
}
74+
75+
public async createAllSessions(
76+
autoInitialize = true,
77+
): Promise<Record<string, MCPSession>> {
78+
const servers = this.config.mcpServers ?? {}
79+
80+
if (Object.keys(servers).length === 0) {
81+
logger.warn('No MCP servers defined in config')
82+
}
83+
84+
for (const name of Object.keys(servers)) {
85+
await this.createSession(name, autoInitialize)
86+
}
87+
88+
return this.sessions
89+
}
90+
91+
public getSession(serverName: string): MCPSession | null {
92+
const session = this.sessions[serverName]
93+
if (!session) {
94+
return null
95+
}
96+
return session
97+
}
98+
99+
public getAllActiveSessions(): Record<string, MCPSession> {
100+
return Object.fromEntries(
101+
this.activeSessions.map(n => [n, this.sessions[n]]),
102+
)
103+
}
104+
105+
public async closeSession(serverName: string): Promise<void> {
106+
const session = this.sessions[serverName]
107+
if (!session) {
108+
logger.warn(`No session exists for server ${serverName}, nothing to close`)
109+
return
110+
}
111+
try {
112+
logger.debug(`Closing session for server ${serverName}`)
113+
await session.disconnect()
114+
}
115+
catch (e) {
116+
logger.error(`Error closing session for server '${serverName}': ${e}`)
117+
}
118+
finally {
119+
delete this.sessions[serverName]
120+
this.activeSessions = this.activeSessions.filter(n => n !== serverName)
121+
}
122+
}
123+
124+
public async closeAllSessions(): Promise<void> {
125+
const serverNames = Object.keys(this.sessions)
126+
const errors: string[] = []
127+
for (const serverName of serverNames) {
128+
try {
129+
logger.debug(`Closing session for server ${serverName}`)
130+
await this.closeSession(serverName)
131+
}
132+
catch (e: any) {
133+
const errorMsg = `Failed to close session for server '${serverName}': ${e}`
134+
logger.error(errorMsg)
135+
errors.push(errorMsg)
136+
}
137+
}
138+
if (errors.length) {
139+
logger.error(`Encountered ${errors.length} errors while closing sessions`)
140+
}
141+
else {
142+
logger.debug('All sessions closed successfully')
143+
}
144+
}
145+
}

src/config-browser.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Browser-safe configuration module that excludes Node.js-specific functionality
3+
*/
4+
5+
import type { BaseConnector } from './connectors/base.js'
6+
import { HttpConnector } from './connectors/http.js'
7+
import { WebSocketConnector } from './connectors/websocket.js'
8+
9+
export function createConnectorFromConfig(
10+
serverConfig: Record<string, any>,
11+
): BaseConnector {
12+
// Only support HTTP and WebSocket connectors in browser environments
13+
if ('url' in serverConfig) {
14+
// HttpConnector automatically handles streamable HTTP with SSE fallback
15+
const transport = serverConfig.transport || 'http'
16+
17+
return new HttpConnector(serverConfig.url, {
18+
headers: serverConfig.headers,
19+
authToken: serverConfig.auth_token || serverConfig.authToken,
20+
// Only force SSE if explicitly requested
21+
preferSse: serverConfig.preferSse || transport === 'sse',
22+
})
23+
}
24+
25+
if ('ws_url' in serverConfig) {
26+
return new WebSocketConnector(serverConfig.ws_url, {
27+
headers: serverConfig.headers,
28+
authToken: serverConfig.auth_token,
29+
})
30+
}
31+
32+
if ('command' in serverConfig && 'args' in serverConfig) {
33+
throw new Error('StdioConnector is not available in browser environments. Use HTTP or WebSocket connectors instead.')
34+
}
35+
36+
throw new Error('Cannot determine connector type from config. Browser environments only support HTTP and WebSocket connectors.')
37+
}

src/config.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,40 @@
11
import type { BaseConnector } from './connectors/base.js'
2-
import { readFileSync } from 'node:fs'
32
import { HttpConnector } from './connectors/http.js'
4-
import { StdioConnector } from './connectors/stdio.js'
53
import { WebSocketConnector } from './connectors/websocket.js'
64

5+
// Check if we're in a Node.js environment
6+
const isNodeJS = typeof process !== 'undefined'
7+
&& process.versions
8+
&& process.versions.node
9+
&& typeof window === 'undefined'
10+
11+
// Conditionally import Node.js-specific modules
12+
let StdioConnector: any
13+
let readFileSync: any
14+
15+
if (isNodeJS) {
16+
try {
17+
// Dynamic import for Node.js-specific modules
18+
Promise.all([
19+
import('./connectors/stdio.js'),
20+
import('node:fs'),
21+
]).then(([stdioModule, fsModule]) => {
22+
StdioConnector = stdioModule.StdioConnector
23+
readFileSync = fsModule.readFileSync
24+
}).catch((error) => {
25+
console.warn('Failed to load Node.js modules:', error)
26+
})
27+
}
28+
catch (error) {
29+
console.warn('Failed to load Node.js modules:', error)
30+
}
31+
}
32+
733
export function loadConfigFile(filepath: string): Record<string, any> {
34+
if (!isNodeJS || !readFileSync) {
35+
throw new Error('loadConfigFile is only available in Node.js environments')
36+
}
37+
838
const raw = readFileSync(filepath, 'utf-8')
939
return JSON.parse(raw)
1040
}
@@ -13,6 +43,10 @@ export function createConnectorFromConfig(
1343
serverConfig: Record<string, any>,
1444
): BaseConnector {
1545
if ('command' in serverConfig && 'args' in serverConfig) {
46+
if (!isNodeJS || !StdioConnector) {
47+
throw new Error('StdioConnector is not available in browser environments. Use HTTP or WebSocket connectors instead.')
48+
}
49+
1650
return new StdioConnector({
1751
command: serverConfig.command,
1852
args: serverConfig.args,
@@ -39,5 +73,5 @@ export function createConnectorFromConfig(
3973
})
4074
}
4175

42-
throw new Error('Cannot determine connector type from config')
76+
throw new Error('Cannot determine connector type from config. Browser environments only support HTTP and WebSocket connectors.')
4377
}

0 commit comments

Comments
 (0)