@h1deya/langchain-mcp-tools 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,18 @@
1
1
  # MCP To LangChain Tools Conversion Utility / TypeScript [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/hideya/langchain-mcp-tools-ts/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/@h1deya/langchain-mcp-tools.svg)](https://www.npmjs.com/package/@h1deya/langchain-mcp-tools)
2
2
 
3
- ## NOTE
3
+ This is a simple, lightweight library intended to simplify the use of
4
+ [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
5
+ server tools with LangChain.
6
+
7
+ Its simplicity and extra features for stdio MCP servers can make it useful as a basis for your own customizations.
8
+ However, it only supports text results of tool calls and does not support MCP features other than tools.
4
9
 
5
- LangChain's official **LangChain.js MCP Adapters** library has been released at:
10
+ LangChain's **official LangChain.js MCP Adapters** library,
11
+ which supports comprehensive integration with LangChain, has been released at:
6
12
  - npmjs: https://www.npmjs.com/package/@langchain/mcp-adapters
7
- - github: https://github.com/langchain-ai/langchainjs-mcp-adapters
13
+ - github: https://github.com/langchain-ai/langchainjs/tree/main/libs/langchain-mcp-adapters`
8
14
 
9
- You may want to consider using the above if you don't have specific needs for using this library...
15
+ You may want to consider using the above if you don't have specific needs for this library.
10
16
 
11
17
  ## Introduction
12
18
 
@@ -14,23 +20,17 @@ This package is intended to simplify the use of
14
20
  [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
15
21
  server tools with LangChain / TypeScript.
16
22
 
17
- [Model Context Protocol (MCP)](https://modelcontextprotocol.io/),
18
- an open standard
19
- [announced by Anthropic](https://www.anthropic.com/news/model-context-protocol),
20
- dramatically expands LLM's scope
21
- by enabling external tool and resource integration, including
22
- GitHub, Google Drive, Slack, Notion, Spotify, Docker, PostgreSQL, and more…
23
+ [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is the de facto industry standard
24
+ that dramatically expands the scope of LLMs by enabling the integration of external tools and resources,
25
+ including DBs, GitHub, Google Drive, Docker, Slack, Notion, Spotify, and more.
23
26
 
24
- MCP is likely to become the de facto industry standard as
25
- [OpenAI has announced its adoption](https://techcrunch.com/2025/03/26/openai-adopts-rival-anthropics-standard-for-connecting-ai-models-to-data).
26
-
27
- Over 2000 functional components available as MCP servers:
27
+ There are quite a few useful MCP servers already available:
28
28
 
29
29
  - [MCP Server Listing on the Official Site](https://github.com/modelcontextprotocol/servers?tab=readme-ov-file#model-context-protocol-servers)
30
30
  - [MCP.so - Find Awesome MCP Servers and Clients](https://mcp.so/)
31
31
  - [Smithery: MCP Server Registry](https://smithery.ai/)
32
32
 
33
- The goal of this utility is to make these 2000+ MCP servers readily accessible from LangChain.
33
+ This utility's goal is to make these massive numbers of MCP servers easily accessible from LangChain.
34
34
 
35
35
  It contains a utility function `convertMcpToLangchainTools()`.
36
36
  This async function handles parallel initialization of specified multiple MCP servers
@@ -94,7 +94,7 @@ The returned tools can be used with LangChain, e.g.:
94
94
 
95
95
  ```ts
96
96
  // import { ChatAnthropic } from "@langchain/anthropic";
97
- const llm = new ChatAnthropic({ model: "claude-3-7-sonnet-latest" });
97
+ const llm = new ChatAnthropic({ model: "claude-sonnet-4-0" });
98
98
 
99
99
  // import { createReactAgent } from "@langchain/langgraph/prebuilt";
100
100
  const agent = createReactAgent({
@@ -109,33 +109,134 @@ try [this LangChain application built with the utility](https://github.com/hidey
109
109
  For detailed information on how to use this library, please refer to the following document:
110
110
  ["Supercharging LangChain: Integrating 2000+ MCP with ReAct"](https://medium.com/@h1deya/supercharging-langchain-integrating-450-mcp-with-react-d4e467cbf41a)
111
111
 
112
- ## Experimental Features
112
+ ## Features
113
+
114
+ ### `stderr` Redirection for Local MCP Server
115
+
116
+ A new key `"stderr"` has been introduced to specify a file descriptor
117
+ to which local (stdio) MCP server's stderr is redirected.
118
+ The key name `stderr` is derived from
119
+ TypeScript SDK's [`StdioServerParameters`](https://github.com/modelcontextprotocol/typescript-sdk/blob/131776764536b5fdca642df51230a3746fb4ade0/src/client/stdio.ts#L32).
120
+
121
+ ```ts
122
+ const logPath = `mcp-server-${serverName}.log`;
123
+ const logFd = fs.openSync(logPath, "w");
124
+ mcpServers[serverName].stderr = logFd;
125
+ ```
126
+
127
+ A usage example can be found [here](
128
+ https://github.com/hideya/langchain-mcp-tools-ts-usage/blob/694b877ed5336bfcd5274d95d3f6d14bed0937a6/src/index.ts#L72-L83)
129
+
130
+ ### Working Directory Configuration for Local MCP Servers
131
+
132
+ The working directory that is used when spawning a local (stdio) MCP server
133
+ can be specified with the `"cwd"` key as follows:
134
+
135
+ ```ts
136
+ "local-server-name": {
137
+ command: "...",
138
+ args: [...],
139
+ cwd: "/working/directory" // the working dir to be use by the server
140
+ },
141
+ ```
142
+
143
+ The key name `cwd` is derived from
144
+ TypeScript SDK's [`StdioServerParameters`](https://github.com/modelcontextprotocol/typescript-sdk/blob/131776764536b5fdca642df51230a3746fb4ade0/src/client/stdio.ts#L39).
145
+
113
146
 
114
147
  ### Remote MCP Server Support
115
148
 
116
- `mcp_servers` configuration for SSE and Websocket servers are as follows:
149
+ `mcp_servers` configuration for Streamable HTTP, SSE and Websocket servers are as follows:
117
150
 
118
151
  ```ts
152
+ // Auto-detection: tries Streamable HTTP first, falls back to SSE on 4xx errors
153
+ "auto-detect-server": {
154
+ url: `http://${server_host}:${server_port}/...`
155
+ },
156
+
157
+ // Explicit Streamable HTTP
158
+ "streamable-http-server": {
159
+ url: `http://${server_host}:${server_port}/...`,
160
+ transport: "streamable_http"
161
+ },
162
+
163
+ // Explicit SSE
119
164
  "sse-server-name": {
120
- url: `http://${sse_server_host}:${sse_server_port}/...`
165
+ url: `http://${sse_server_host}:${sse_server_port}/...`,
166
+ transport: "sse"
121
167
  },
122
168
 
169
+ // WebSocket
123
170
  "ws-server-name": {
124
171
  url: `ws://${ws_server_host}:${ws_server_port}/...`
125
172
  },
126
173
  ```
127
174
 
128
- Note that the key `"url"` may be changed in the future to match
129
- the MCP server configurations used by Claude for Desktop once
130
- it introduces remote server support.
175
+ **Auto-detection behavior (default):**
176
+ - For HTTP/HTTPS URLs without explicit `transport`, the library follows [MCP specification recommendations](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#backwards-compatibility)
177
+ - First attempts Streamable HTTP transport
178
+ - If Streamable HTTP fails with a 4xx error, automatically falls back to SSE transport
179
+ - Non-4xx errors (network issues, etc.) are re-thrown without fallback
131
180
 
132
- A usage example can be found [here](
133
- https://github.com/hideya/langchain-mcp-tools-ts-usage/blob/694b877ed5336bfcd5274d95d3f6d14bed0937a6/src/index.ts#L26-L38)
181
+ **Explicit transport selection:**
182
+ - Set `transport: "streamable_http"` to force Streamable HTTP (no fallback)
183
+ - Set `transport: "sse"` to force SSE transport
184
+ - WebSocket URLs (`ws://` or `wss://`) always use WebSocket transport
185
+
186
+ Streamable HTTP is the modern MCP transport that replaces the older HTTP+SSE transport. According to the [official MCP documentation](https://modelcontextprotocol.io/docs/concepts/transports):
187
+
188
+ > "SSE as a standalone transport is deprecated as of protocol version 2024-11-05. It has been replaced by Streamable HTTP, which incorporates SSE as an optional streaming mechanism."
189
+
190
+ Note that even when you specify the Streamable HTTP transport, you may see SSE activity in the logs, such as `Accept: text/event-stream`.
191
+ This occurs when the MCP SDK chooses to use SSE for streaming server responses within the Streamable HTTP transport.
134
192
 
135
- ### Authentication Support for SSE Connections
193
+ ### Authentication Support for Streamable HTTP Connections
136
194
 
137
- The library now supports authentication for SSE connections to MCP servers.
138
- This is particularly useful for accessing authenticated MCP servers that require OAuth.
195
+ The library supports OAuth 2.1 authentication for Streamable HTTP connections:
196
+
197
+ ```ts
198
+ import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
199
+
200
+ // Implement your own OAuth client provider
201
+ class MyOAuthProvider implements OAuthClientProvider {
202
+ // Implementation details...
203
+ }
204
+
205
+ const mcpServers = {
206
+ "secure-streamable-server": {
207
+ url: "https://secure-mcp-server.example.com/mcp",
208
+ transport: "streamable_http", // Optional: explicit transport
209
+ streamableHTTPOptions: {
210
+ // Provide an OAuth client provider
211
+ authProvider: new MyOAuthProvider(),
212
+
213
+ // Optionally customize HTTP requests
214
+ requestInit: {
215
+ headers: {
216
+ 'X-Custom-Header': 'custom-value'
217
+ }
218
+ },
219
+
220
+ // Optionally configure reconnection behavior
221
+ reconnectionOptions: {
222
+ maxReconnectAttempts: 5,
223
+ reconnectDelay: 1000
224
+ }
225
+ }
226
+ }
227
+ };
228
+ ```
229
+
230
+ Test implementations are provided:
231
+
232
+ - **Streamable HTTP Authentication Tests**:
233
+ - MCP client uses this library: [streamable-http-auth-test-client.ts](https://github.com/hideya/langchain-mcp-tools-ts/tree/main/testfiles/streamable-http-auth-test-client.ts)
234
+ - Test MCP Server: [streamable-http-auth-test-server.ts](https://github.com/hideya/langchain-mcp-tools-ts/tree/main/testfiles/streamable-http-auth-test-server.ts)
235
+
236
+ ### Authentication Support for SSE Connections (Legacy)
237
+
238
+ The library also supports authentication for SSE connections to MCP servers.
239
+ Note that SSE transport is deprecated; Streamable HTTP is the recommended approach.
139
240
 
140
241
  To enable authentication, provide SSE options in your server configuration:
141
242
 
@@ -170,47 +271,11 @@ const mcpServers = {
170
271
  };
171
272
  ```
172
273
 
173
- A simple example showing how to implement an OAuth client provider can be found
174
- in [sse-auth-test-client.ts](https://github.com/hideya/langchain-mcp-tools-ts-usage/tree/main/src/sse-auth-test-client.ts)
175
- of [this usage examples repo](https://github.com/hideya/langchain-mcp-tools-ts-usage).
176
-
177
- For testing purposes, a sample MCP server with OAuth authentication support
178
- that works with the above client is provided
179
- in [sse-auth-test-server.ts](https://github.com/hideya/langchain-mcp-tools-ts-usage/tree/main/src/sse-auth-test-server.ts)
180
- of [this usage examples repo](https://github.com/hideya/langchain-mcp-tools-ts-usage).
181
-
182
- ### Working Directory Configuration for Local MCP Servers
183
-
184
- The working directory that is used when spawning a local (stdio) MCP server
185
- can be specified with the `"cwd"` key as follows:
186
-
187
- ```ts
188
- "local-server-name": {
189
- command: "...",
190
- args: [...],
191
- cwd: "/working/directory" // the working dir to be use by the server
192
- },
193
- ```
194
-
195
- The key name `cwd` is derived from
196
- TypeScript SDK's [`StdioServerParameters`](https://github.com/modelcontextprotocol/typescript-sdk/blob/131776764536b5fdca642df51230a3746fb4ade0/src/client/stdio.ts#L39).
197
-
198
-
199
- ### `stderr` Redirection for Local MCP Server
200
-
201
- A new key `"stderr"` has been introduced to specify a file descriptor
202
- to which local (stdio) MCP server's stderr is redirected.
203
- The key name `stderr` is derived from
204
- TypeScript SDK's [`StdioServerParameters`](https://github.com/modelcontextprotocol/typescript-sdk/blob/131776764536b5fdca642df51230a3746fb4ade0/src/client/stdio.ts#L32).
274
+ Test implementations are provided:
205
275
 
206
- ```ts
207
- const logPath = `mcp-server-${serverName}.log`;
208
- const logFd = fs.openSync(logPath, "w");
209
- mcpServers[serverName].stderr = logFd;
210
- ```
211
-
212
- A usage example can be found [here](
213
- https://github.com/hideya/langchain-mcp-tools-ts-usage/blob/694b877ed5336bfcd5274d95d3f6d14bed0937a6/src/index.ts#L72-L83)
276
+ - **SSE Authentication Tests**:
277
+ - MCP client uses this library: [sse-auth-test-client.ts](https://github.com/hideya/langchain-mcp-tools-ts/tree/main/testfiles/sse-auth-test-client.ts)
278
+ - Test MCP Server: [sse-auth-test-server.ts](https://github.com/hideya/langchain-mcp-tools-ts/tree/main/testfiles/sse-auth-test-server.ts)
214
279
 
215
280
  ## Limitations
216
281
 
@@ -1,6 +1,7 @@
1
1
  import { IOType } from "node:child_process";
2
2
  import { Stream } from "node:stream";
3
3
  import { StructuredTool } from "@langchain/core/tools";
4
+ import { StreamableHTTPReconnectionOptions } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
5
  import { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
5
6
  /**
6
7
  * Configuration for a command-line based MCP server.
@@ -10,6 +11,7 @@ import { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
10
11
  */
11
12
  export interface CommandBasedConfig {
12
13
  url?: never;
14
+ transport?: never;
13
15
  command: string;
14
16
  args?: string[];
15
17
  env?: Record<string, string>;
@@ -24,6 +26,7 @@ export interface CommandBasedConfig {
24
26
  */
25
27
  export interface UrlBasedConfig {
26
28
  url: string;
29
+ transport?: string;
27
30
  command?: never;
28
31
  args?: never;
29
32
  env?: never;
@@ -34,6 +37,12 @@ export interface UrlBasedConfig {
34
37
  eventSourceInit?: EventSourceInit;
35
38
  requestInit?: RequestInit;
36
39
  };
40
+ streamableHTTPOptions?: {
41
+ authProvider?: OAuthClientProvider;
42
+ requestInit?: RequestInit;
43
+ reconnectionOptions?: StreamableHTTPReconnectionOptions;
44
+ sessionId?: string;
45
+ };
37
46
  }
38
47
  /**
39
48
  * Configuration for an MCP server.
@@ -1,10 +1,12 @@
1
1
  import { DynamicStructuredTool } from "@langchain/core/tools";
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
5
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
6
  import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
6
7
  import { CallToolResultSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
7
8
  import { jsonSchemaToZod } from "@n8n/json-schema-to-zod";
9
+ import { z } from "zod";
8
10
  import { Logger } from "./logger.js";
9
11
  // Custom error type for MCP server initialization failures
10
12
  /**
@@ -82,6 +84,278 @@ export async function convertMcpToLangchainTools(configs, options) {
82
84
  allTools.forEach((tool) => logger.debug(`- ${tool.name}`));
83
85
  return { tools: allTools, cleanup };
84
86
  }
87
+ /**
88
+ * Sanitizes a JSON Schema to make it compatible with Google Gemini API.
89
+ *
90
+ * ⚠️ IMPORTANT: This is a temporary workaround to keep applications running.
91
+ * The underlying schema compatibility issues should be fixed on the MCP server side
92
+ * for proper Google LLM compatibility. This function will log warnings when
93
+ * performing schema conversions to help track which servers need upstream fixes.
94
+ *
95
+ * Gemini supports only a limited subset of OpenAPI 3.0 Schema properties:
96
+ * - string: enum, format (only 'date-time' documented)
97
+ * - integer/number: format only
98
+ * - array: minItems, maxItems, items
99
+ * - object: properties, required, propertyOrdering, nullable
100
+ *
101
+ * This function removes known problematic properties while preserving
102
+ * as much validation as possible. When debug logging is enabled,
103
+ * it reports what schema changes were made for transparency.
104
+ *
105
+ * Reference: https://ai.google.dev/gemini-api/docs/structured-output#json-schemas
106
+ *
107
+ * @param schema - The JSON schema to sanitize
108
+ * @param logger - Optional logger for reporting sanitization actions
109
+ * @param toolName - Optional tool name for logging context
110
+ * @returns A sanitized schema compatible with all major LLM providers
111
+ *
112
+ * @internal This function is meant to be used internally by convertSingleMcpToLangchainTools
113
+ */
114
+ function sanitizeSchemaForGemini(schema, logger, toolName) {
115
+ if (typeof schema !== 'object' || schema === null) {
116
+ return schema;
117
+ }
118
+ const sanitized = { ...schema };
119
+ const removedProperties = [];
120
+ const convertedProperties = [];
121
+ // Remove unsupported properties
122
+ if (sanitized.exclusiveMinimum !== undefined) {
123
+ removedProperties.push('exclusiveMinimum');
124
+ delete sanitized.exclusiveMinimum;
125
+ }
126
+ if (sanitized.exclusiveMaximum !== undefined) {
127
+ removedProperties.push('exclusiveMaximum');
128
+ delete sanitized.exclusiveMaximum;
129
+ }
130
+ // Convert exclusiveMinimum/Maximum to minimum/maximum if needed
131
+ if (schema.exclusiveMinimum !== undefined) {
132
+ sanitized.minimum = schema.exclusiveMinimum;
133
+ convertedProperties.push('exclusiveMinimum → minimum');
134
+ }
135
+ if (schema.exclusiveMaximum !== undefined) {
136
+ sanitized.maximum = schema.exclusiveMaximum;
137
+ convertedProperties.push('exclusiveMaximum → maximum');
138
+ }
139
+ // Remove unsupported string formats (Gemini only supports 'enum' and 'date-time')
140
+ if (sanitized.type === 'string' && sanitized.format) {
141
+ const supportedFormats = ['enum', 'date-time'];
142
+ if (!supportedFormats.includes(sanitized.format)) {
143
+ removedProperties.push(`format: ${sanitized.format}`);
144
+ delete sanitized.format;
145
+ }
146
+ }
147
+ // Log sanitization actions for this level
148
+ if (logger && toolName && (removedProperties.length > 0 || convertedProperties.length > 0)) {
149
+ const changes = [];
150
+ if (removedProperties.length > 0) {
151
+ changes.push(`removed: ${removedProperties.join(', ')}`);
152
+ }
153
+ if (convertedProperties.length > 0) {
154
+ changes.push(`converted: ${convertedProperties.join(', ')}`);
155
+ }
156
+ logger.warn(`MCP tool "${toolName}": schema sanitized for Gemini compatibility (${changes.join('; ')})`);
157
+ }
158
+ // Recursively process nested objects and arrays
159
+ if (sanitized.properties) {
160
+ sanitized.properties = Object.fromEntries(Object.entries(sanitized.properties).map(([key, value]) => [
161
+ key,
162
+ sanitizeSchemaForGemini(value, logger, toolName)
163
+ ]));
164
+ }
165
+ if (sanitized.anyOf) {
166
+ sanitized.anyOf = sanitized.anyOf.map((subSchema) => sanitizeSchemaForGemini(subSchema, logger, toolName));
167
+ }
168
+ if (sanitized.oneOf) {
169
+ sanitized.oneOf = sanitized.oneOf.map((subSchema) => sanitizeSchemaForGemini(subSchema, logger, toolName));
170
+ }
171
+ if (sanitized.allOf) {
172
+ sanitized.allOf = sanitized.allOf.map((subSchema) => sanitizeSchemaForGemini(subSchema, logger, toolName));
173
+ }
174
+ if (sanitized.items) {
175
+ sanitized.items = sanitizeSchemaForGemini(sanitized.items, logger, toolName);
176
+ }
177
+ if (sanitized.additionalProperties && typeof sanitized.additionalProperties === 'object') {
178
+ sanitized.additionalProperties = sanitizeSchemaForGemini(sanitized.additionalProperties, logger, toolName);
179
+ }
180
+ return sanitized;
181
+ }
182
+ /**
183
+ * Creates Streamable HTTP transport options from configuration.
184
+ * Consolidates repeated option configuration logic into a single reusable function.
185
+ *
186
+ * @param config - URL-based server configuration
187
+ * @param logger - Logger instance for recording authentication setup
188
+ * @param serverName - Server name for logging context
189
+ * @returns Configured StreamableHTTPClientTransportOptions or undefined if no options needed
190
+ *
191
+ * @internal This function is meant to be used internally by transport creation functions
192
+ */
193
+ function createStreamableHttpOptions(config, logger, serverName) {
194
+ const options = {};
195
+ if (config.streamableHTTPOptions) {
196
+ if (config.streamableHTTPOptions.authProvider) {
197
+ options.authProvider = config.streamableHTTPOptions.authProvider;
198
+ logger.info(`MCP server "${serverName}": configuring Streamable HTTP with authentication provider`);
199
+ }
200
+ if (config.streamableHTTPOptions.requestInit) {
201
+ options.requestInit = config.streamableHTTPOptions.requestInit;
202
+ }
203
+ if (config.streamableHTTPOptions.reconnectionOptions) {
204
+ options.reconnectionOptions = config.streamableHTTPOptions.reconnectionOptions;
205
+ }
206
+ if (config.streamableHTTPOptions.sessionId) {
207
+ options.sessionId = config.streamableHTTPOptions.sessionId;
208
+ }
209
+ }
210
+ return Object.keys(options).length > 0 ? options : undefined;
211
+ }
212
+ /**
213
+ * Creates SSE transport options from configuration.
214
+ * Consolidates repeated option configuration logic into a single reusable function.
215
+ *
216
+ * @param config - URL-based server configuration
217
+ * @param logger - Logger instance for recording authentication setup
218
+ * @param serverName - Server name for logging context
219
+ * @returns Configured SSEClientTransportOptions or undefined if no options needed
220
+ *
221
+ * @internal This function is meant to be used internally by transport creation functions
222
+ */
223
+ function createSseOptions(config, logger, serverName) {
224
+ const options = {};
225
+ if (config.sseOptions) {
226
+ if (config.sseOptions.authProvider) {
227
+ options.authProvider = config.sseOptions.authProvider;
228
+ logger.info(`MCP server "${serverName}": configuring SSE with authentication provider`);
229
+ }
230
+ if (config.sseOptions.eventSourceInit) {
231
+ options.eventSourceInit = config.sseOptions.eventSourceInit;
232
+ }
233
+ if (config.sseOptions.requestInit) {
234
+ options.requestInit = config.sseOptions.requestInit;
235
+ }
236
+ }
237
+ return Object.keys(options).length > 0 ? options : undefined;
238
+ }
239
+ /**
240
+ * Determines if an error represents a 4xx HTTP status code.
241
+ * Used to decide whether to fall back from Streamable HTTP to SSE transport.
242
+ *
243
+ * @param error - The error to check
244
+ * @returns true if the error represents a 4xx HTTP status
245
+ *
246
+ * @internal This function is meant to be used internally by createHttpTransportWithFallback
247
+ */
248
+ function is4xxError(error) {
249
+ if (!error || typeof error !== 'object') {
250
+ return false;
251
+ }
252
+ // Check for common error patterns that indicate 4xx responses
253
+ const errorObj = error;
254
+ // Check if it's a fetch Response error with status
255
+ if (errorObj.status && typeof errorObj.status === 'number') {
256
+ return errorObj.status >= 400 && errorObj.status < 500;
257
+ }
258
+ // Check if it's wrapped in a Response object
259
+ if (errorObj.response && errorObj.response.status && typeof errorObj.response.status === 'number') {
260
+ return errorObj.response.status >= 400 && errorObj.response.status < 500;
261
+ }
262
+ // Check for error messages that typically indicate 4xx errors
263
+ const message = errorObj.message || errorObj.toString();
264
+ if (typeof message === 'string') {
265
+ return /4[0-9]{2}/.test(message) ||
266
+ message.includes('Bad Request') ||
267
+ message.includes('Unauthorized') ||
268
+ message.includes('Forbidden') ||
269
+ message.includes('Not Found') ||
270
+ message.includes('Method Not Allowed');
271
+ }
272
+ return false;
273
+ }
274
+ /**
275
+ * Creates an HTTP transport with automatic fallback from Streamable HTTP to SSE.
276
+ * Follows the MCP specification recommendation to try Streamable HTTP first,
277
+ * then fall back to SSE if a 4xx error is encountered.
278
+ *
279
+ * See: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#backwards-compatibility
280
+ *
281
+ * @param url - The URL to connect to
282
+ * @param config - URL-based server configuration
283
+ * @param logger - Logger instance for recording connection attempts
284
+ * @param serverName - Server name for logging context
285
+ * @returns A promise that resolves to a configured Transport
286
+ *
287
+ * @internal This function is meant to be used internally by convertSingleMcpToLangchainTools
288
+ */
289
+ async function createHttpTransportWithFallback(url, config, logger, serverName) {
290
+ // If transport is explicitly specified, respect user's choice
291
+ if (config.transport === "streamable_http") {
292
+ logger.debug(`MCP server "${serverName}": using explicitly configured Streamable HTTP transport`);
293
+ const options = createStreamableHttpOptions(config, logger, serverName);
294
+ return new StreamableHTTPClientTransport(url, options);
295
+ }
296
+ if (config.transport === "sse") {
297
+ logger.debug(`MCP server "${serverName}": using explicitly configured SSE transport`);
298
+ const options = createSseOptions(config, logger, serverName);
299
+ return new SSEClientTransport(url, options);
300
+ }
301
+ // Auto-detection: try Streamable HTTP first, fall back to SSE on 4xx errors
302
+ logger.debug(`MCP server "${serverName}": attempting Streamable HTTP transport with SSE fallback`);
303
+ try {
304
+ const options = createStreamableHttpOptions(config, logger, serverName);
305
+ const transport = new StreamableHTTPClientTransport(url, options);
306
+ logger.info(`MCP server "${serverName}": successfully created Streamable HTTP transport`);
307
+ return transport;
308
+ }
309
+ catch (error) {
310
+ if (is4xxError(error)) {
311
+ logger.info(`MCP server "${serverName}": Streamable HTTP failed with 4xx error, falling back to SSE transport`);
312
+ const options = createSseOptions(config, logger, serverName);
313
+ return new SSEClientTransport(url, options);
314
+ }
315
+ // Re-throw non-4xx errors (network issues, etc.)
316
+ logger.error(`MCP server "${serverName}": Streamable HTTP transport creation failed with non-4xx error:`, error);
317
+ throw error;
318
+ }
319
+ }
320
+ /**
321
+ * Transforms a Zod schema to be compatible with OpenAI's Structured Outputs requirements.
322
+ *
323
+ * OpenAI's Structured Outputs feature requires that all optional fields must also be nullable.
324
+ * This function converts Zod schemas that use `.optional()` or `.default()` to also include
325
+ * `.nullable()`, ensuring compatibility with OpenAI models while maintaining compatibility
326
+ * with other LLM providers like Anthropic.
327
+ * See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required
328
+ *
329
+ * @param schema - The Zod object schema to transform
330
+ * @returns A new Zod schema with optional/default fields made nullable
331
+ *
332
+ * @example
333
+ * // Input schema: z.object({ name: z.string(), age: z.number().optional() })
334
+ * // Output schema: z.object({ name: z.string(), age: z.number().optional().nullable() })
335
+ *
336
+ * @see {@link https://platform.openai.com/docs/guides/structured-outputs | OpenAI Structured Outputs Documentation}
337
+ *
338
+ * @internal This function is meant to be used internally by convertSingleMcpToLangchainTools
339
+ */
340
+ function makeZodSchemaOpenAICompatible(schema) {
341
+ const shape = schema.shape;
342
+ const newShape = {};
343
+ for (const [key, value] of Object.entries(shape)) {
344
+ if (value instanceof z.ZodOptional && !(value instanceof z.ZodNullable)) {
345
+ // Convert .optional() to .optional().nullable() for OpenAI compatibility
346
+ newShape[key] = value.nullable();
347
+ }
348
+ else if (value instanceof z.ZodDefault && !(value instanceof z.ZodNullable)) {
349
+ // Convert .default() to .default().nullable() for OpenAI compatibility
350
+ newShape[key] = value.nullable();
351
+ }
352
+ else {
353
+ // Keep existing fields unchanged (including already nullable fields)
354
+ newShape[key] = value;
355
+ }
356
+ }
357
+ return z.object(newShape);
358
+ }
85
359
  /**
86
360
  * Initializes a single MCP server and converts its capabilities into LangChain tools.
87
361
  * Sets up a connection to the server, retrieves available tools, and creates corresponding
@@ -113,22 +387,67 @@ async function convertSingleMcpToLangchainTools(serverName, config, logger) {
113
387
  // Ignore
114
388
  }
115
389
  if (url?.protocol === "http:" || url?.protocol === "https:") {
116
- // Extract SSE options from config if available
390
+ // Use the new auto-detection logic with fallback
117
391
  const urlConfig = config;
118
- const sseOptions = {};
119
- if (urlConfig.sseOptions) {
120
- if (urlConfig.sseOptions.authProvider) {
121
- sseOptions.authProvider = urlConfig.sseOptions.authProvider;
122
- logger.info(`MCP server "${serverName}": configuring SSE with authentication provider`);
123
- }
124
- if (urlConfig.sseOptions.eventSourceInit) {
125
- sseOptions.eventSourceInit = urlConfig.sseOptions.eventSourceInit;
392
+ // Try to connect with Streamable HTTP first, fallback to SSE on 4xx errors
393
+ let connectionSucceeded = false;
394
+ // If transport is explicitly specified, respect user's choice (no fallback)
395
+ if (urlConfig.transport === "streamable_http" || urlConfig.transport === "sse") {
396
+ transport = await createHttpTransportWithFallback(url, urlConfig, logger, serverName);
397
+ }
398
+ else {
399
+ // Auto-detection with connection-level fallback
400
+ logger.debug(`MCP server "${serverName}": attempting Streamable HTTP transport with SSE fallback`);
401
+ try {
402
+ // First attempt: Streamable HTTP
403
+ const options = createStreamableHttpOptions(urlConfig, logger, serverName);
404
+ transport = new StreamableHTTPClientTransport(url, options);
405
+ logger.info(`MCP server "${serverName}": created Streamable HTTP transport, attempting connection`);
406
+ // Try to connect with Streamable HTTP
407
+ client = new Client({
408
+ name: "mcp-client",
409
+ version: "0.0.1",
410
+ }, {
411
+ capabilities: {},
412
+ });
413
+ await client.connect(transport);
414
+ connectionSucceeded = true;
415
+ logger.info(`MCP server "${serverName}": successfully connected using Streamable HTTP`);
126
416
  }
127
- if (urlConfig.sseOptions.requestInit) {
128
- sseOptions.requestInit = urlConfig.sseOptions.requestInit;
417
+ catch (error) {
418
+ if (is4xxError(error)) {
419
+ logger.info(`MCP server "${serverName}": Streamable HTTP failed with 4xx error, falling back to SSE transport`);
420
+ // Cleanup failed transport and client
421
+ if (transport) {
422
+ try {
423
+ await transport.close();
424
+ }
425
+ catch (cleanupError) {
426
+ logger.debug(`MCP server "${serverName}": cleanup error during fallback:`, cleanupError);
427
+ }
428
+ }
429
+ // Fallback to SSE
430
+ const options = createSseOptions(urlConfig, logger, serverName);
431
+ transport = new SSEClientTransport(url, options);
432
+ logger.info(`MCP server "${serverName}": created SSE transport, attempting fallback connection`);
433
+ // Create new client for SSE connection
434
+ client = new Client({
435
+ name: "mcp-client",
436
+ version: "0.0.1",
437
+ }, {
438
+ capabilities: {},
439
+ });
440
+ await client.connect(transport);
441
+ connectionSucceeded = true;
442
+ logger.info(`MCP server "${serverName}": successfully connected using SSE fallback`);
443
+ }
444
+ else {
445
+ // Re-throw non-4xx errors (network issues, etc.)
446
+ logger.error(`MCP server "${serverName}": Streamable HTTP transport failed with non-4xx error:`, error);
447
+ throw error;
448
+ }
129
449
  }
130
450
  }
131
- transport = new SSEClientTransport(url, Object.keys(sseOptions).length > 0 ? sseOptions : undefined);
132
451
  }
133
452
  else if (url?.protocol === "ws:" || url?.protocol === "wss:") {
134
453
  transport = new WebSocketClientTransport(url);
@@ -150,57 +469,67 @@ async function convertSingleMcpToLangchainTools(serverName, config, logger) {
150
469
  cwd: stdioServerConfig.cwd
151
470
  });
152
471
  }
153
- client = new Client({
154
- name: "mcp-client",
155
- version: "0.0.1",
156
- }, {
157
- capabilities: {},
158
- });
159
- await client.connect(transport);
160
- logger.info(`MCP server "${serverName}": connected`);
472
+ // Only create client if not already created during auto-detection fallback
473
+ if (!client) {
474
+ client = new Client({
475
+ name: "mcp-client",
476
+ version: "0.0.1",
477
+ }, {
478
+ capabilities: {},
479
+ });
480
+ await client.connect(transport);
481
+ logger.info(`MCP server "${serverName}": connected`);
482
+ }
161
483
  const toolsResponse = await client.request({ method: "tools/list" }, ListToolsResultSchema);
162
- const tools = toolsResponse.tools.map((tool) => (new DynamicStructuredTool({
163
- name: tool.name,
164
- description: tool.description || "",
165
- // FIXME
166
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
- schema: jsonSchemaToZod(tool.inputSchema),
168
- func: async function (input) {
169
- logger.info(`MCP tool "${serverName}"/"${tool.name}" received input:`, input);
170
- try {
171
- // Execute tool call
172
- const result = await client?.request({
173
- method: "tools/call",
174
- params: {
175
- name: tool.name,
176
- arguments: input,
177
- },
178
- }, CallToolResultSchema);
179
- // Handles null/undefined cases gracefully
180
- if (!result?.content) {
181
- logger.info(`MCP tool "${serverName}"/"${tool.name}" received null/undefined result`);
182
- return "";
484
+ const tools = toolsResponse.tools.map((tool) => {
485
+ // Apply sanitization for all LLMs (harmless for non-Gemini providers)
486
+ const sanitizedSchema = sanitizeSchemaForGemini(tool.inputSchema, logger, `${serverName}/${tool.name}`);
487
+ const baseSchema = jsonSchemaToZod(sanitizedSchema);
488
+ // Transforms a Zod schema to be compatible with OpenAI's Structured Outputs requirements.
489
+ const compatibleSchema = makeZodSchemaOpenAICompatible(baseSchema);
490
+ return new DynamicStructuredTool({
491
+ name: tool.name,
492
+ description: tool.description || "",
493
+ // FIXME
494
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
495
+ schema: compatibleSchema,
496
+ func: async function (input) {
497
+ logger.info(`MCP tool "${serverName}"/"${tool.name}" received input:`, input);
498
+ try {
499
+ // Execute tool call
500
+ const result = await client?.request({
501
+ method: "tools/call",
502
+ params: {
503
+ name: tool.name,
504
+ arguments: input,
505
+ },
506
+ }, CallToolResultSchema);
507
+ // Handles null/undefined cases gracefully
508
+ if (!result?.content) {
509
+ logger.info(`MCP tool "${serverName}"/"${tool.name}" received null/undefined result`);
510
+ return "";
511
+ }
512
+ const textContent = result.content
513
+ .filter(content => content.type === "text")
514
+ .map(content => content.text)
515
+ .join("\n\n");
516
+ // const textItems = result.content
517
+ // .filter(content => content.type === "text")
518
+ // .map(content => content.text)
519
+ // const textContent = JSON.stringify(textItems);
520
+ // Log rough result size for monitoring
521
+ const size = new TextEncoder().encode(textContent).length;
522
+ logger.info(`MCP tool "${serverName}"/"${tool.name}" received result (size: ${size})`);
523
+ // If no text content, return a clear message describing the situation
524
+ return textContent || "No text content available in response";
183
525
  }
184
- const textContent = result.content
185
- .filter(content => content.type === "text")
186
- .map(content => content.text)
187
- .join("\n\n");
188
- // const textItems = result.content
189
- // .filter(content => content.type === "text")
190
- // .map(content => content.text)
191
- // const textContent = JSON.stringify(textItems);
192
- // Log rough result size for monitoring
193
- const size = new TextEncoder().encode(textContent).length;
194
- logger.info(`MCP tool "${serverName}"/"${tool.name}" received result (size: ${size})`);
195
- // If no text content, return a clear message describing the situation
196
- return textContent || "No text content available in response";
197
- }
198
- catch (error) {
199
- logger.warn(`MCP tool "${serverName}"/"${tool.name}" caused error: ${error}`);
200
- return `Error executing MCP tool: ${error}`;
201
- }
202
- },
203
- })));
526
+ catch (error) {
527
+ logger.warn(`MCP tool "${serverName}"/"${tool.name}" caused error: ${error}`);
528
+ return `Error executing MCP tool: ${error}`;
529
+ }
530
+ },
531
+ });
532
+ });
204
533
  logger.info(`MCP server "${serverName}": ${tools.length} tool(s) available:`);
205
534
  tools.forEach((tool) => logger.info(`- ${tool.name}`));
206
535
  async function cleanup() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h1deya/langchain-mcp-tools",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "MCP To LangChain Tools Conversion Utility",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -30,22 +30,39 @@
30
30
  "url": "git+https://github.com/hideya/langchain-mcp-tools-ts.git"
31
31
  },
32
32
  "scripts": {
33
+ "_comment_build": "# Build and development scripts",
33
34
  "build": "tsc",
34
35
  "prepare": "npm run build",
35
36
  "watch": "tsc --watch",
36
- "simple-usage": "tsx testfiles/simple-usage.ts",
37
- "direct-test": "tsx -- testfiles/direct-test.ts",
38
- "sse-auth-test-client": "tsx testfiles/sse-auth-test-client.ts",
39
- "sse-auth-test-server": "tsx testfiles/sse-auth-test-server.ts",
40
37
  "lint": "eslint src",
38
+ "clean": "git clean -fdxn -e .env && read -p 'OK?' && git clean -fdx -e .env",
39
+
40
+ "_comment_test": "# Testing scripts",
41
41
  "test": "vitest run",
42
42
  "test:watch": "vitest",
43
43
  "test:coverage": "vitest run --coverage",
44
- "typedoc": "npx typedoc --options typedoc.json",
45
- "deploy-docs": "npm run typedoc && ghp-import -n -p -f docs",
46
- "clean": "git clean -fdxn -e .env && read -p 'OK?' && git clean -fdx -e .env",
47
- "do-publish": "npm run clean && npm install && npm publish --access=public",
48
- "test-publish": "npm run clean && npm install && npm publish --access=public --dry-run"
44
+
45
+ "_comment_examples": "# Basic usage examples",
46
+ "example:simple": "tsx testfiles/simple-usage.ts",
47
+
48
+ "_comment_streamable": "# Streamable HTTP transport tests",
49
+ "test:streamable:auth:server": "tsx testfiles/streamable-http-auth-test-server.ts",
50
+ "test:streamable:auth:client": "tsx testfiles/streamable-http-auth-test-client.ts",
51
+ "test:streamable:stateless:server": "tsx testfiles/streamable-http-stateless-test-server.ts",
52
+ "test:streamable:stateless:client": "tsx testfiles/streamable-http-stateless-test-client.ts",
53
+ "test:streamable:auto-detection": "tsx testfiles/streamable-http-auto-detection-test.ts",
54
+
55
+ "_comment_sse": "# SSE transport tests (with authentication)",
56
+ "test:sse:server": "tsx testfiles/sse-auth-test-server.ts",
57
+ "test:sse:client": "tsx testfiles/sse-auth-test-client.ts",
58
+
59
+ "_comment_docs": "# Documentation scripts",
60
+ "docs:build": "npx typedoc --options typedoc.json",
61
+ "docs:deploy": "npm run docs:build && ghp-import -n -p -f docs",
62
+
63
+ "_comment_publish": "# Publishing scripts",
64
+ "publish:test": "npm run clean && npm install && npm publish --access=public --dry-run",
65
+ "publish:do": "npm run clean && npm install && npm publish --access=public"
49
66
  },
50
67
  "dependencies": {
51
68
  "@langchain/core": "^0.3.27",
@@ -56,6 +73,7 @@
56
73
  "devDependencies": {
57
74
  "@eslint/js": "^9.17.0",
58
75
  "@langchain/anthropic": "^0.3.11",
76
+ "@langchain/google-genai": "^0.2.12",
59
77
  "@langchain/langgraph": "^0.2.36",
60
78
  "@langchain/openai": "^0.3.16",
61
79
  "@types/node": "^22.10.5",