From e2ce0924054c032ed04ba5795c03aa258a1e17b1 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Mon, 15 Sep 2025 14:48:03 +0200 Subject: [PATCH] fix: make sure tool calls are processed sequentially --- src/Mutex.ts | 46 ++++++++++++++++++++++++++++++++++++++ src/index.ts | 63 +++++++++++++++++++++++++++++----------------------- 2 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 src/Mutex.ts diff --git a/src/Mutex.ts b/src/Mutex.ts new file mode 100644 index 00000000..78c01633 --- /dev/null +++ b/src/Mutex.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2025 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export class Mutex { + static Guard = class Guard { + #mutex: Mutex; + #onRelease?: () => void; + constructor(mutex: Mutex, onRelease?: () => void) { + this.#mutex = mutex; + this.#onRelease = onRelease; + } + dispose(): void { + this.#onRelease?.(); + return this.#mutex.release(); + } + }; + + #locked = false; + #acquirers: Array<() => void> = []; + + // This is FIFO. + async acquire( + onRelease?: () => void, + ): Promise> { + if (!this.#locked) { + this.#locked = true; + return new Mutex.Guard(this); + } + const {resolve, promise} = Promise.withResolvers(); + this.#acquirers.push(resolve); + await promise; + return new Mutex.Guard(this, onRelease); + } + + release(): void { + const resolve = this.#acquirers.shift(); + if (!resolve) { + this.#locked = false; + return; + } + resolve(); + } +} diff --git a/src/index.ts b/src/index.ts index 8b192028..6895cb05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ import * as snapshotTools from './tools/snapshot.js'; import path from 'node:path'; import fs from 'node:fs'; import assert from 'node:assert'; +import {Mutex} from './Mutex.js'; export const cliOptions = { browserUrl: { @@ -169,6 +170,8 @@ Avoid sharing sensitive or personal information that you do want to share with M ); }; +const toolMutex = new Mutex(); + function registerTool(tool: ToolDefinition): void { server.registerTool( tool.name, @@ -178,35 +181,39 @@ function registerTool(tool: ToolDefinition): void { annotations: tool.annotations, }, async (params): Promise => { - logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`); - const context = await getContext(); - const response = new McpResponse(); - await tool.handler( - { - params, - }, - response, - context, - ); + const guard = await toolMutex.acquire(); try { - const content = await response.handle(tool.name, context); - - return { - content, - }; - } catch (error) { - const errorText = - error instanceof Error ? error.message : String(error); - - return { - content: [ - { - type: 'text', - text: errorText, - }, - ], - isError: true, - }; + logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`); + const context = await getContext(); + const response = new McpResponse(); + await tool.handler( + { + params, + }, + response, + context, + ); + try { + const content = await response.handle(tool.name, context); + return { + content, + }; + } catch (error) { + const errorText = + error instanceof Error ? error.message : String(error); + + return { + content: [ + { + type: 'text', + text: errorText, + }, + ], + isError: true, + }; + } + } finally { + guard.dispose(); } }, );