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
This commit is contained in:
Dmitry Zhuravlev
2026-03-27 09:26:28 +00:00
committed by intellij-monorepo-bot
parent 142141f4b2
commit f829dcca8e
4 changed files with 118 additions and 16 deletions
+1
View File
@@ -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).
+102 -14
View File
@@ -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;
@@ -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
})
+13 -2
View File
@@ -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<typeof createProjectPathManager>
private readonly _defaultProjectPathKey: 'project_path' | 'projectPath'
private readonly _toolCallTimeoutMs: number
private readonly _buildTimeoutMs: number
private readonly _warn: (message: string) => void
private _connectedPromise: Promise<void> | 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<unknown> {
return await this.withReconnect(method, async () => {