From f829dcca8e28c78fba3f19bc54e28feb15fdc722 Mon Sep 17 00:00:00 2001 From: Dmitry Zhuravlev Date: Thu, 26 Mar 2026 20:19:52 +0100 Subject: [PATCH] misc [ij-mcp-proxy]: increase build_project timeout to 20 minutes The default 60s tool call timeout is too short for build_project, which can take many minutes on large projects. Add a separate JETBRAINS_MCP_BUILD_TIMEOUT_S env var (default 1200s / 20 minutes) that applies specifically to build_project calls, keeping the general tool call timeout unchanged. GitOrigin-RevId: 6688f659c32adcbab83beeba256680d6a9dd6147 --- build/mcp-servers/ij-proxy/README.md | 1 + .../ij-proxy/dist/ij-mcp-proxy.mjs | 116 +++++++++++++++--- build/mcp-servers/ij-proxy/ij-mcp-proxy.ts | 2 + build/mcp-servers/ij-proxy/upstream.ts | 15 ++- 4 files changed, 118 insertions(+), 16 deletions(-) diff --git a/build/mcp-servers/ij-proxy/README.md b/build/mcp-servers/ij-proxy/README.md index fcebe45a16ab..a31e1932ab8f 100644 --- a/build/mcp-servers/ij-proxy/README.md +++ b/build/mcp-servers/ij-proxy/README.md @@ -55,6 +55,7 @@ Environment variables (optional): - `JETBRAINS_MCP_SCAN_TIMEOUT_S`: timeout for additional port probes after the default port fails (seconds). Default: `1`. Use `0` to disable. - `JETBRAINS_MCP_QUEUE_LIMIT`: max number of queued client messages before the stream endpoint is ready. Default: `100`. Use `0` for unlimited. - `JETBRAINS_MCP_TOOL_CALL_TIMEOUT_S`: timeout for upstream tool calls after they are sent (seconds). Default: `60`. Use `0` to disable. +- `JETBRAINS_MCP_BUILD_TIMEOUT_S`: timeout for `build_project` upstream calls (seconds). Default: `1200` (20 minutes). Use `0` to disable. - `JETBRAINS_MCP_QUEUE_WAIT_TIMEOUT_S`: timeout for upstream tool calls waiting to be sent while the stream is unavailable (seconds). Defaults to the tool-call timeout when set; use `0` to disable. - `JETBRAINS_MCP_PROJECT_PATH`: override the injected project path (defaults to `process.cwd()`, relative paths resolve from the current working directory, and `file://` URIs are supported). - `MCP_LOG`: path to a log file for proxy progress (cleared on startup). diff --git a/build/mcp-servers/ij-proxy/dist/ij-mcp-proxy.mjs b/build/mcp-servers/ij-proxy/dist/ij-mcp-proxy.mjs index a147c8bef39a..1ef237c23de9 100644 --- a/build/mcp-servers/ij-proxy/dist/ij-mcp-proxy.mjs +++ b/build/mcp-servers/ij-proxy/dist/ij-mcp-proxy.mjs @@ -19094,6 +19094,8 @@ class Protocol { clearTimeout(info.timeoutId), this._timeoutInfo.delete(messageId); } async connect(transport) { + if (this._transport) + throw Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection."); this._transport = transport; let _onclose = this.transport?.onclose; this._transport.onclose = () => { @@ -19118,6 +19120,9 @@ class Protocol { _onclose() { let responseHandlers = this._responseHandlers; this._responseHandlers = /* @__PURE__ */ new Map, this._progressHandlers.clear(), this._taskProgressTokens.clear(), this._pendingDebouncedNotifications.clear(); + for (let controller of this._requestHandlerAbortControllers.values()) + controller.abort(); + this._requestHandlerAbortControllers.clear(); let error48 = McpError.fromError(ErrorCode.ConnectionClosed, "Connection closed"); this._transport = void 0, this.onclose?.(); for (let handler of responseHandlers.values()) @@ -19160,12 +19165,16 @@ class Protocol { sessionId: capturedTransport?.sessionId, _meta: request.params?._meta, sendNotification: async (notification) => { + if (abortController.signal.aborted) + return; let notificationOptions = { relatedRequestId: request.id }; if (relatedTaskId) notificationOptions.relatedTask = { taskId: relatedTaskId }; await this.notification(notification, notificationOptions); }, sendRequest: async (r, resultSchema, options) => { + if (abortController.signal.aborted) + throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled"); let requestOptions = { ...options, relatedRequestId: request.id }; if (relatedTaskId && !requestOptions.relatedTask) requestOptions.relatedTask = { taskId: relatedTaskId }; @@ -19694,6 +19703,49 @@ class ExperimentalServerTasks { requestStream(request, resultSchema, options) { return this._server.requestStream(request, resultSchema, options); } + createMessageStream(params, options) { + let clientCapabilities = this._server.getClientCapabilities(); + if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) + throw Error("Client does not support sampling tools capability."); + if (params.messages.length > 0) { + let lastMessage = params.messages[params.messages.length - 1], lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content], hasToolResults = lastContent.some((c) => c.type === "tool_result"), previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : void 0, previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [], hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use"); + if (hasToolResults) { + if (lastContent.some((c) => c.type !== "tool_result")) + throw Error("The last message must contain only tool_result content if any is present"); + if (!hasPreviousToolUse) + throw Error("tool_result blocks are not matching any tool_use from the previous message"); + } + if (hasPreviousToolUse) { + let toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id)), toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId)); + if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) + throw Error("ids of tool_result blocks and tool_use blocks from previous message do not match"); + } + } + return this.requestStream({ + method: "sampling/createMessage", + params + }, CreateMessageResultSchema, options); + } + elicitInputStream(params, options) { + let clientCapabilities = this._server.getClientCapabilities(), mode = params.mode ?? "form"; + switch (mode) { + case "url": { + if (!clientCapabilities?.elicitation?.url) + throw Error("Client does not support url elicitation."); + break; + } + case "form": { + if (!clientCapabilities?.elicitation?.form) + throw Error("Client does not support form elicitation."); + break; + } + } + let normalizedParams = mode === "form" && params.mode === void 0 ? { ...params, mode: "form" } : params; + return this.requestStream({ + method: "elicitation/create", + params: normalizedParams + }, ElicitResultSchema, options); + } async getTask(taskId, options) { return this._server.getTask({ taskId }, options); } @@ -20668,16 +20720,31 @@ async function auth(provider, options) { } } async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) { - let resourceMetadata, authorizationServerUrl; - try { - if (resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl }, fetchFn), resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) - authorizationServerUrl = resourceMetadata.authorization_servers[0]; - } catch {} - if (!authorizationServerUrl) - authorizationServerUrl = new URL("/", serverUrl); - let resource = await selectResourceURL(serverUrl, provider, resourceMetadata), metadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, { - fetchFn - }), clientInformation = await Promise.resolve(provider.clientInformation()); + let cachedState = await provider.discoveryState?.(), resourceMetadata, authorizationServerUrl, metadata, effectiveResourceMetadataUrl = resourceMetadataUrl; + if (!effectiveResourceMetadataUrl && cachedState?.resourceMetadataUrl) + effectiveResourceMetadataUrl = new URL(cachedState.resourceMetadataUrl); + if (cachedState?.authorizationServerUrl) { + if (authorizationServerUrl = cachedState.authorizationServerUrl, resourceMetadata = cachedState.resourceMetadata, metadata = cachedState.authorizationServerMetadata ?? await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn }), !resourceMetadata) + try { + resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn); + } catch {} + if (metadata !== cachedState.authorizationServerMetadata || resourceMetadata !== cachedState.resourceMetadata) + await provider.saveDiscoveryState?.({ + authorizationServerUrl: String(authorizationServerUrl), + resourceMetadataUrl: effectiveResourceMetadataUrl?.toString(), + resourceMetadata, + authorizationServerMetadata: metadata + }); + } else { + let serverInfo = await discoverOAuthServerInfo(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl, fetchFn }); + authorizationServerUrl = serverInfo.authorizationServerUrl, metadata = serverInfo.authorizationServerMetadata, resourceMetadata = serverInfo.resourceMetadata, await provider.saveDiscoveryState?.({ + authorizationServerUrl: String(authorizationServerUrl), + resourceMetadataUrl: effectiveResourceMetadataUrl?.toString(), + resourceMetadata, + authorizationServerMetadata: metadata + }); + } + let resource = await selectResourceURL(serverUrl, provider, resourceMetadata), clientInformation = await Promise.resolve(provider.clientInformation()); if (!clientInformation) { if (authorizationCode !== void 0) throw Error("Existing OAuth client information is required when exchanging an authorization code"); @@ -20881,6 +20948,21 @@ async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fet } return; } +async function discoverOAuthServerInfo(serverUrl, opts) { + let resourceMetadata, authorizationServerUrl; + try { + if (resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn), resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) + authorizationServerUrl = resourceMetadata.authorization_servers[0]; + } catch {} + if (!authorizationServerUrl) + authorizationServerUrl = String(new URL("/", serverUrl)); + let authorizationServerMetadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn: opts?.fetchFn }); + return { + authorizationServerUrl, + authorizationServerMetadata, + resourceMetadata + }; +} async function startAuthorization(authorizationServerUrl, { metadata, clientInformation, redirectUrl, scope, state, resource }) { let authorizationUrl; if (metadata) { @@ -24474,6 +24556,7 @@ class UpstreamConnection { _projectPathManager; _defaultProjectPathKey; _toolCallTimeoutMs; + _buildTimeoutMs; _warn; _connectedPromise = null; _tools = null; @@ -24482,7 +24565,7 @@ class UpstreamConnection { ideVersion = null; onStateChange; constructor(options) { - this._transport = options.transport, this._toolCallTimeoutMs = options.toolCallTimeoutMs, this._warn = options.warn, this._defaultProjectPathKey = options.defaultProjectPathKey, this._projectPathManager = createProjectPathManager({ + this._transport = options.transport, this._toolCallTimeoutMs = options.toolCallTimeoutMs, this._buildTimeoutMs = options.buildTimeoutMs, this._warn = options.warn, this._defaultProjectPathKey = options.defaultProjectPathKey, this._projectPathManager = createProjectPathManager({ projectPath: options.projectPath, defaultProjectPathKey: options.defaultProjectPathKey }), this.client = new Client({ name: "ij-mcp-proxy", version: "1.0.0" }), this.client.onerror = (error48) => { @@ -24543,7 +24626,7 @@ class UpstreamConnection { await this.connect(), await this.getTools(); let callArgs = { ...args }; this._projectPathManager.injectProjectPathArgs(toolName, callArgs); - let options = this._toolCallTimeoutMs > 0 ? { timeout: this._toolCallTimeoutMs } : void 0, result = normalizeToolResult(await this.client.callTool({ name: toolName, arguments: callArgs }, void 0, options)); + let timeoutMs = this._resolveTimeoutMs(toolName), options = timeoutMs > 0 ? { timeout: timeoutMs } : void 0, result = normalizeToolResult(await this.client.callTool({ name: toolName, arguments: callArgs }, void 0, options)); if (result?.isError) throw Error(extractTextFromResult(result) || "Upstream tool error"); return result; @@ -24552,10 +24635,14 @@ class UpstreamConnection { async callToolForClient(toolName, args) { return await this.withReconnect(`tools/call ${toolName}`, async () => { await this.connect(), await this.getTools(), this._projectPathManager.injectProjectPathArgs(toolName, args); - let options = this._toolCallTimeoutMs > 0 ? { timeout: this._toolCallTimeoutMs } : void 0, result = await this.client.callTool({ name: toolName, arguments: args }, void 0, options); + let timeoutMs = this._resolveTimeoutMs(toolName), options = timeoutMs > 0 ? { timeout: timeoutMs } : void 0, result = await this.client.callTool({ name: toolName, arguments: args }, void 0, options); return normalizeToolResult(result); }); } + static _LONG_TIMEOUT_TOOLS = /* @__PURE__ */ new Set(["build_project"]); + _resolveTimeoutMs(toolName) { + return UpstreamConnection._LONG_TIMEOUT_TOOLS.has(toolName) ? this._buildTimeoutMs : this._toolCallTimeoutMs; + } async forwardRequest(method, params) { return await this.withReconnect(method, async () => { return await this.connect(), await this.client.request({ method, params }, ResultSchema); @@ -24676,7 +24763,7 @@ function extractPathArg(args) { } // community/build/mcp-servers/ij-proxy/ij-mcp-proxy.ts -var explicitMcpUrl = env.JETBRAINS_MCP_STREAM_URL || env.MCP_STREAM_URL || env.JETBRAINS_MCP_URL || env.MCP_URL, defaultHost = "127.0.0.1", defaultPort = 64342, defaultPath = "/stream", defaultScanLimit = 10, portScanStartEnv = env.JETBRAINS_MCP_PORT_START, portScanStart = parseEnvInt("JETBRAINS_MCP_PORT_START", defaultPort), portScanLimit = parseEnvInt("JETBRAINS_MCP_PORT_SCAN_LIMIT", defaultScanLimit), preferredPorts = portScanStartEnv ? [portScanStart] : [defaultPort, 64344], connectTimeoutMs = parseEnvSeconds("JETBRAINS_MCP_CONNECT_TIMEOUT_S", 10), scanTimeoutMs = parseEnvSeconds("JETBRAINS_MCP_SCAN_TIMEOUT_S", 1), queueLimit = parseEnvNonNegativeInt("JETBRAINS_MCP_QUEUE_LIMIT", 100), toolCallTimeoutMs = parseEnvSeconds("JETBRAINS_MCP_TOOL_CALL_TIMEOUT_S", 60), queueWaitTimeoutMs = parseEnvSeconds("JETBRAINS_MCP_QUEUE_WAIT_TIMEOUT_S", toolCallTimeoutMs > 0 ? Math.round(toolCallTimeoutMs / 1000) : 0), STREAM_RETRY_ATTEMPTS = 3, STREAM_RETRY_BASE_DELAY_MS = 200; +var explicitMcpUrl = env.JETBRAINS_MCP_STREAM_URL || env.MCP_STREAM_URL || env.JETBRAINS_MCP_URL || env.MCP_URL, defaultHost = "127.0.0.1", defaultPort = 64342, defaultPath = "/stream", defaultScanLimit = 10, portScanStartEnv = env.JETBRAINS_MCP_PORT_START, portScanStart = parseEnvInt("JETBRAINS_MCP_PORT_START", defaultPort), portScanLimit = parseEnvInt("JETBRAINS_MCP_PORT_SCAN_LIMIT", defaultScanLimit), preferredPorts = portScanStartEnv ? [portScanStart] : [defaultPort, 64344], connectTimeoutMs = parseEnvSeconds("JETBRAINS_MCP_CONNECT_TIMEOUT_S", 10), scanTimeoutMs = parseEnvSeconds("JETBRAINS_MCP_SCAN_TIMEOUT_S", 1), queueLimit = parseEnvNonNegativeInt("JETBRAINS_MCP_QUEUE_LIMIT", 100), toolCallTimeoutMs = parseEnvSeconds("JETBRAINS_MCP_TOOL_CALL_TIMEOUT_S", 60), buildTimeoutMs = parseEnvSeconds("JETBRAINS_MCP_BUILD_TIMEOUT_S", 1200), queueWaitTimeoutMs = parseEnvSeconds("JETBRAINS_MCP_QUEUE_WAIT_TIMEOUT_S", toolCallTimeoutMs > 0 ? Math.round(toolCallTimeoutMs / 1000) : 0), STREAM_RETRY_ATTEMPTS = 3, STREAM_RETRY_BASE_DELAY_MS = 200; function parseEnvInt(name, fallback) { let raw = env[name]; if (!raw) @@ -24799,6 +24886,7 @@ function createUpstreamForUrl(url2) { projectPath, defaultProjectPathKey, toolCallTimeoutMs, + buildTimeoutMs, warn }); return conn.onStateChange = () => updateProxyTooling(), conn; diff --git a/build/mcp-servers/ij-proxy/ij-mcp-proxy.ts b/build/mcp-servers/ij-proxy/ij-mcp-proxy.ts index d91dca23ff51..b55b2bbc3c7b 100644 --- a/build/mcp-servers/ij-proxy/ij-mcp-proxy.ts +++ b/build/mcp-servers/ij-proxy/ij-mcp-proxy.ts @@ -40,6 +40,7 @@ const connectTimeoutMs = parseEnvSeconds('JETBRAINS_MCP_CONNECT_TIMEOUT_S', 10) const scanTimeoutMs = parseEnvSeconds('JETBRAINS_MCP_SCAN_TIMEOUT_S', 1) const queueLimit = parseEnvNonNegativeInt('JETBRAINS_MCP_QUEUE_LIMIT', 100) const toolCallTimeoutMs = parseEnvSeconds('JETBRAINS_MCP_TOOL_CALL_TIMEOUT_S', 60) +const buildTimeoutMs = parseEnvSeconds('JETBRAINS_MCP_BUILD_TIMEOUT_S', 20 * 60) const queueWaitTimeoutMs = parseEnvSeconds( 'JETBRAINS_MCP_QUEUE_WAIT_TIMEOUT_S', toolCallTimeoutMs > 0 ? Math.round(toolCallTimeoutMs / 1000) : 0 @@ -229,6 +230,7 @@ function createUpstreamForUrl(url: string): UpstreamConnection { projectPath, defaultProjectPathKey, toolCallTimeoutMs, + buildTimeoutMs, warn }) diff --git a/build/mcp-servers/ij-proxy/upstream.ts b/build/mcp-servers/ij-proxy/upstream.ts index c0a78c09950d..5f62d6304755 100644 --- a/build/mcp-servers/ij-proxy/upstream.ts +++ b/build/mcp-servers/ij-proxy/upstream.ts @@ -13,6 +13,7 @@ export interface UpstreamConnectionOptions { projectPath: string defaultProjectPathKey: 'project_path' | 'projectPath' toolCallTimeoutMs: number + buildTimeoutMs: number warn: (message: string) => void } @@ -39,6 +40,7 @@ export class UpstreamConnection { private _projectPathManager: ReturnType private readonly _defaultProjectPathKey: 'project_path' | 'projectPath' private readonly _toolCallTimeoutMs: number + private readonly _buildTimeoutMs: number private readonly _warn: (message: string) => void private _connectedPromise: Promise | null = null @@ -54,6 +56,7 @@ export class UpstreamConnection { constructor(options: UpstreamConnectionOptions) { this._transport = options.transport this._toolCallTimeoutMs = options.toolCallTimeoutMs + this._buildTimeoutMs = options.buildTimeoutMs this._warn = options.warn this._defaultProjectPathKey = options.defaultProjectPathKey this._projectPathManager = createProjectPathManager({ @@ -149,7 +152,8 @@ export class UpstreamConnection { await this.getTools() const callArgs = {...args} this._projectPathManager.injectProjectPathArgs(toolName, callArgs) - const options = this._toolCallTimeoutMs > 0 ? {timeout: this._toolCallTimeoutMs} : undefined + const timeoutMs = this._resolveTimeoutMs(toolName) + const options = timeoutMs > 0 ? {timeout: timeoutMs} : undefined const result = normalizeToolResult( await this.client.callTool({name: toolName, arguments: callArgs}, undefined, options) ) @@ -167,12 +171,19 @@ export class UpstreamConnection { await this.connect() await this.getTools() this._projectPathManager.injectProjectPathArgs(toolName, args) - const options = this._toolCallTimeoutMs > 0 ? {timeout: this._toolCallTimeoutMs} : undefined + const timeoutMs = this._resolveTimeoutMs(toolName) + const options = timeoutMs > 0 ? {timeout: timeoutMs} : undefined const result = await this.client.callTool({name: toolName, arguments: args}, undefined, options) return normalizeToolResult(result) }) } + private static readonly _LONG_TIMEOUT_TOOLS = new Set(['build_project']) + + private _resolveTimeoutMs(toolName: string): number { + return UpstreamConnection._LONG_TIMEOUT_TOOLS.has(toolName) ? this._buildTimeoutMs : this._toolCallTimeoutMs + } + /** Forward arbitrary request to upstream. */ async forwardRequest(method: string, params: unknown): Promise { return await this.withReconnect(method, async () => {