diff --git a/.husky/pre-commit b/.husky/pre-commit index a845b850..3cfc2a14 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -npm run lint \ No newline at end of file +npm run lint:fix && npm run lint \ No newline at end of file diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index f3d68140..00000000 --- a/examples/README.md +++ /dev/null @@ -1,194 +0,0 @@ -# MCP-Use TypeScript Examples - -This directory contains examples demonstrating how to use the mcp-use library with various MCP servers. - -## Prerequisites - -1. **Node.js**: Ensure you have Node.js 22+ installed -2. **Environment Variables**: Create a `.env` file in the project root with your API keys: - ```bash - ANTHROPIC_API_KEY=your_anthropic_key - OPENAI_API_KEY=your_openai_key - E2B_API_KEY=your_e2b_key # Only for sandbox example - ``` - -## Running Examples - -First, build the library: - -```bash -npm run build -``` - -Then run any example using Node.js: - -```bash -node dist/examples/example_name.js -``` - -Or use the npm scripts: - -```bash -npm run example:airbnb -npm run example:browser -npm run example:chat -# ... etc -``` - -## Available Examples - -### 1. Airbnb Search (`airbnb_use.ts`) - -Search for accommodations using the Airbnb MCP server. - -```bash -npm run example:airbnb -``` - -### 2. Browser Automation (`browser_use.ts`) - -Control a browser using Playwright MCP server. - -```bash -npm run example:browser -``` - -### 3. Interactive Chat (`chat_example.ts`) - -Interactive chat session with conversation memory. - -```bash -npm run example:chat -``` - -### 4. File System Operations (`filesystem_use.ts`) - -Access and manipulate files using the filesystem MCP server. - -```bash -# First, edit the example to set your directory path -npm run example:filesystem -``` - -### 5. HTTP Server Connection (`http_example.ts`) - -Connect to an MCP server via HTTP. - -```bash -# First, start the Playwright server in another terminal: -npx @playwright/mcp@latest --port 8931 - -# Then run the example: -npm run example:http -``` - -### 6. MCP Everything Test (`mcp_everything.ts`) - -Test various MCP functionalities. - -```bash -npm run example:everything -``` - -### 7. Multiple Servers (`multi_server_example.ts`) - -Use multiple MCP servers in a single session. - -```bash -# First, edit the example to set your directory path -npm run example:multi -``` - -### 8. Sandboxed Environment (`sandbox_everything.ts`) - -Run MCP servers in an E2B sandbox (requires E2B_API_KEY). - -```bash -npm run example:sandbox -``` - -### 9. OAuth Authentication (`simple_oauth_example.ts`) - -OAuth flow example with Linear. - -```bash -# First, register your app with Linear and update the client_id -npm run example:oauth -``` - -### 10. Blender Integration (`blender_use.ts`) - -Control Blender 3D through MCP. - -```bash -# First, install and enable the Blender MCP addon -npm run example:blender -``` - -## Configuration Files - -Some examples use JSON configuration files: - -- `airbnb_mcp.json` - Airbnb server configuration -- `browser_mcp.json` - Browser server configuration - -## Environment Variables - -Different examples require different API keys: - -- **ANTHROPIC_API_KEY**: For examples using Claude (airbnb, multi_server, blender) -- **OPENAI_API_KEY**: For examples using GPT (browser, chat, filesystem, http, everything) -- **E2B_API_KEY**: Only for the sandbox example - -## Troubleshooting - -1. **Module not found**: Make sure to build the project first with `npm run build` -2. **API key errors**: Check your `.env` file has the required keys -3. **Server connection failed**: Some examples require external servers to be running -4. **Permission errors**: Some examples may need specific permissions (e.g., filesystem access) - -## Writing Your Own Examples - -To create a new example: - -1. Import the necessary modules: - - ```typescript - import { ChatOpenAI } from '@langchain/openai' - import { MCPAgent, MCPClient } from '../index.js' - ``` - -2. Configure your MCP server: - - ```typescript - const config = { - mcpServers: { - yourServer: { - command: 'npx', - args: ['your-mcp-server'] - } - } - } - ``` - -3. Create client, LLM, and agent: - - ```typescript - const client = MCPClient.fromDict(config) - const llm = new ChatOpenAI({ model: 'gpt-4o' }) - const agent = new MCPAgent({ llm, client, maxSteps: 30 }) - ``` - -4. Run your queries: - ```typescript - const result = await agent.run('Your prompt here', { maxSteps: 30 }) - ``` - -## Contributing - -Feel free to add more examples! Make sure to: - -1. Follow the existing code style -2. Add appropriate documentation -3. Update this README with your example -4. Add a corresponding npm script in package.json diff --git a/examples/http_example.ts b/examples/http_example.ts index 106fcfaf..d243a3bb 100644 --- a/examples/http_example.ts +++ b/examples/http_example.ts @@ -23,7 +23,7 @@ import { MCPAgent, MCPClient } from '../index.js' config() async function main() { - const config = { mcpServers: { http: { url: 'https://hf.com/mcp' } } } + const config = { mcpServers: { http: { url: 'https://gitmcp.io/docs' } } } // Create MCPClient from config const client = MCPClient.fromDict(config) @@ -36,10 +36,12 @@ async function main() { // Run the query const result = await agent.run( - 'Find the best restaurant in San Francisco USING GOOGLE SEARCH', + 'Which tools are available and what can they do?', 30, ) console.log(`\nResult: ${result}`) + + await agent.close() } if (import.meta.url === `file://${process.argv[1]}`) { diff --git a/src/config.ts b/src/config.ts index 28163dbc..e6f44809 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,9 +21,14 @@ export function createConnectorFromConfig( } if ('url' in serverConfig) { + // HttpConnector automatically handles streamable HTTP with SSE fallback + const transport = serverConfig.transport || 'http' + return new HttpConnector(serverConfig.url, { headers: serverConfig.headers, - authToken: serverConfig.auth_token, + authToken: serverConfig.auth_token || serverConfig.authToken, + // Only force SSE if explicitly requested + preferSse: serverConfig.preferSse || transport === 'sse', }) } diff --git a/src/connectors/http.ts b/src/connectors/http.ts index df4cc0e6..d0a3a906 100644 --- a/src/connectors/http.ts +++ b/src/connectors/http.ts @@ -1,7 +1,9 @@ import type { ConnectorInitOptions } from './base.js' import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { logger } from '../logging.js' import { SseConnectionManager } from '../task_managers/sse.js' +import { StreamableHttpConnectionManager } from '../task_managers/streamable_http.js' import { BaseConnector } from './base.js' export interface HttpConnectorOptions extends ConnectorInitOptions { @@ -10,6 +12,7 @@ export interface HttpConnectorOptions extends ConnectorInitOptions { timeout?: number // HTTP request timeout (s) sseReadTimeout?: number // SSE read timeout (s) clientInfo?: { name: string, version: string } + preferSse?: boolean // Force SSE transport instead of trying streamable HTTP first } export class HttpConnector extends BaseConnector { @@ -18,6 +21,8 @@ export class HttpConnector extends BaseConnector { private readonly timeout: number private readonly sseReadTimeout: number private readonly clientInfo: { name: string, version: string } + private readonly preferSse: boolean + private transportType: 'streamable-http' | 'sse' | null = null constructor(baseUrl: string, opts: HttpConnectorOptions = {}) { super(opts) @@ -31,24 +36,116 @@ export class HttpConnector extends BaseConnector { this.timeout = opts.timeout ?? 5 this.sseReadTimeout = opts.sseReadTimeout ?? 60 * 5 this.clientInfo = opts.clientInfo ?? { name: 'http-connector', version: '1.0.0' } + this.preferSse = opts.preferSse ?? false } - /** Establish connection to the MCP implementation via SSE. */ + /** Establish connection to the MCP implementation via HTTP (streamable or SSE). */ async connect(): Promise { if (this.connected) { logger.debug('Already connected to MCP implementation') return } - logger.debug(`Connecting to MCP implementation via HTTP/SSE: ${this.baseUrl}`) + const baseUrl = this.baseUrl + // If preferSse is set, skip directly to SSE + if (this.preferSse) { + logger.debug(`Connecting to MCP implementation via HTTP/SSE: ${baseUrl}`) + await this.connectWithSse(baseUrl) + return + } + + // Try streamable HTTP first, then fall back to SSE + logger.debug(`Connecting to MCP implementation via HTTP: ${baseUrl}`) + + try { + // Try streamable HTTP transport first + logger.debug('Attempting streamable HTTP transport...') + await this.connectWithStreamableHttp(baseUrl) + } + catch (err) { + // Check if this is a 4xx error that indicates we should try SSE fallback + let fallbackReason = 'Unknown error' + + if (err instanceof StreamableHTTPError) { + if (err.code === 404 || err.code === 405) { + fallbackReason = `Server returned ${err.code} - server likely doesn't support streamable HTTP` + logger.debug(fallbackReason) + } + else { + fallbackReason = `Server returned ${err.code}: ${err.message}` + logger.debug(fallbackReason) + } + } + else if (err instanceof Error) { + // Check for 404/405 in error message as fallback detection + const errorStr = err.toString() + if (errorStr.includes('405 Method Not Allowed') || errorStr.includes('404 Not Found')) { + fallbackReason = 'Server doesn\'t support streamable HTTP (405/404)' + logger.debug(fallbackReason) + } + else { + fallbackReason = `Streamable HTTP failed: ${err.message}` + logger.debug(fallbackReason) + } + } + + // Always try SSE fallback for maximum compatibility + logger.debug('Falling back to SSE transport...') + + try { + await this.connectWithSse(baseUrl) + } + catch (sseErr) { + logger.error(`Failed to connect with both transports:`) + logger.error(` Streamable HTTP: ${fallbackReason}`) + logger.error(` SSE: ${sseErr}`) + await this.cleanupResources() + throw new Error('Could not connect to server with any available transport') + } + } + } + + private async connectWithStreamableHttp(baseUrl: string): Promise { try { - // Build the SSE URL (root of server endpoint) - const sseUrl = this.baseUrl + // Create and start the streamable HTTP connection manager + this.connectionManager = new StreamableHttpConnectionManager( + baseUrl, + { + requestInit: { + headers: this.headers, + }, + // Pass through timeout and other options + reconnectionOptions: { + maxReconnectionDelay: 30000, + initialReconnectionDelay: 1000, + reconnectionDelayGrowFactor: 1.5, + maxRetries: 2, + }, + }, + ) + const transport = await this.connectionManager.start() + + // Create and connect the client + this.client = new Client(this.clientInfo, this.opts.clientOptions) + await this.client.connect(transport) + + this.connected = true + this.transportType = 'streamable-http' + logger.debug(`Successfully connected to MCP implementation via streamable HTTP: ${baseUrl}`) + } + catch (err) { + // Clean up partial resources before throwing + await this.cleanupResources() + throw err + } + } - // Create and start the connection manager -> returns an SSE transport + private async connectWithSse(baseUrl: string): Promise { + try { + // Create and start the SSE connection manager this.connectionManager = new SseConnectionManager( - sseUrl, + baseUrl, { requestInit: { headers: this.headers, @@ -62,10 +159,11 @@ export class HttpConnector extends BaseConnector { await this.client.connect(transport) this.connected = true - logger.debug(`Successfully connected to MCP implementation via HTTP/SSE: ${this.baseUrl}`) + this.transportType = 'sse' + logger.debug(`Successfully connected to MCP implementation via HTTP/SSE: ${baseUrl}`) } catch (err) { - logger.error(`Failed to connect to MCP implementation via HTTP/SSE: ${err}`) + // Clean up partial resources before throwing await this.cleanupResources() throw err } @@ -75,6 +173,14 @@ export class HttpConnector extends BaseConnector { return { type: 'http', url: this.baseUrl, + transport: this.transportType || 'unknown', } } + + /** + * Get the transport type being used (streamable-http or sse) + */ + getTransportType(): 'streamable-http' | 'sse' | null { + return this.transportType + } } diff --git a/src/task_managers/index.ts b/src/task_managers/index.ts index 28ccfcc4..a5d81751 100644 --- a/src/task_managers/index.ts +++ b/src/task_managers/index.ts @@ -1,4 +1,5 @@ export { ConnectionManager } from './base.js' export { SseConnectionManager } from './sse.js' export { StdioConnectionManager } from './stdio.js' +export { StreamableHttpConnectionManager } from './streamable_http.js' export { WebSocketConnectionManager } from './websocket.js' diff --git a/src/task_managers/streamable_http.ts b/src/task_managers/streamable_http.ts new file mode 100644 index 00000000..60d33724 --- /dev/null +++ b/src/task_managers/streamable_http.ts @@ -0,0 +1,57 @@ +import type { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' +import { logger } from '../logging.js' +import { ConnectionManager } from './base.js' + +export class StreamableHttpConnectionManager extends ConnectionManager { + private readonly url: URL + private readonly opts?: StreamableHTTPClientTransportOptions + private _transport: StreamableHTTPClientTransport | null = null + + /** + * Create a Streamable HTTP connection manager. + * + * @param url The HTTP endpoint URL. + * @param opts Optional transport options (auth, headers, etc.). + */ + constructor(url: string | URL, opts?: StreamableHTTPClientTransportOptions) { + super() + this.url = typeof url === 'string' ? new URL(url) : url + this.opts = opts + } + + /** + * Spawn a new `StreamableHTTPClientTransport` and return it. + * The Client.connect() method will handle starting the transport. + */ + protected async establishConnection(): Promise { + this._transport = new StreamableHTTPClientTransport(this.url, this.opts) + + logger.debug(`${this.constructor.name} created successfully`) + return this._transport + } + + /** + * Close the underlying transport and clean up resources. + */ + protected async closeConnection(_connection: StreamableHTTPClientTransport): Promise { + if (this._transport) { + try { + await this._transport.close() + } + catch (e) { + logger.warn(`Error closing Streamable HTTP transport: ${e}`) + } + finally { + this._transport = null + } + } + } + + /** + * Get the session ID from the transport if available. + */ + get sessionId(): string | undefined { + return this._transport?.sessionId + } +}