From 1c4bd5272d3c5538f193bd66427e9cc8c43fc09e Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Fri, 23 Jan 2026 11:15:40 +0000 Subject: [PATCH 1/6] Implement list_extensions tool for local unpacked extensions --- src/McpContext.ts | 16 ++++++++-- src/tools/ToolDefinition.ts | 2 ++ src/tools/extensions.ts | 27 +++++++++++++++++ src/utils/ExtensionRegistry.ts | 55 ++++++++++++++++++++++++++++++++++ tests/tools/extensions.test.ts | 33 ++++++++++++++++++++ 5 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 src/utils/ExtensionRegistry.ts diff --git a/src/McpContext.ts b/src/McpContext.ts index 557f012fa..98f223d4d 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -35,6 +35,7 @@ import {takeSnapshot} from './tools/snapshot.js'; import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js'; import type {Context, DevToolsData} from './tools/ToolDefinition.js'; import type {TraceResult} from './trace-processing/parse.js'; +import { ExtensionRegistry, type InstalledExtension } from './utils/ExtensionRegistry.js'; import {WaitForHelper} from './WaitForHelper.js'; export interface TextSnapshotNode extends SerializedAXNode { @@ -112,6 +113,7 @@ export class McpContext implements Context { #networkCollector: NetworkCollector; #consoleCollector: ConsoleCollector; #devtoolsUniverseManager: UniverseManager; + #extensionRegistry = new ExtensionRegistry(); #isRunningTrace = false; #networkConditionsMap = new WeakMap(); @@ -753,11 +755,19 @@ export class McpContext implements Context { await this.#networkCollector.init(await this.browser.pages()); } - async installExtension(path: string): Promise { - return this.browser.installExtension(path); + async installExtension(extensionPath: string): Promise { + const id = await this.browser.installExtension(extensionPath); + await this.#extensionRegistry.registerExtension(id, extensionPath); + return id; } async uninstallExtension(id: string): Promise { - return this.browser.uninstallExtension(id); + await this.browser.uninstallExtension(id); + this.#extensionRegistry.remove(id); + } + + listExtensions(): InstalledExtension[] { + return this.#extensionRegistry.list(); } } + diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 1cfa9751f..93431f917 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -13,6 +13,7 @@ import type { Viewport, } from '../third_party/index.js'; import type {InsightName, TraceResult} from '../trace-processing/parse.js'; +import type { InstalledExtension } from '../utils/ExtensionRegistry.js'; import type {PaginationOptions} from '../utils/types.js'; import type {ToolCategory} from './categories.js'; @@ -141,6 +142,7 @@ export type Context = Readonly<{ resolveCdpElementId(cdpBackendNodeId: number): string | undefined; installExtension(path: string): Promise; uninstallExtension(id: string): Promise; + listExtensions(): InstalledExtension[]; }>; export function defineTool( diff --git a/src/tools/extensions.ts b/src/tools/extensions.ts index 42ca85f43..2bfc154e3 100644 --- a/src/tools/extensions.ts +++ b/src/tools/extensions.ts @@ -22,6 +22,7 @@ export const installExtension = defineTool({ schema: { path: zod .string() + .trim() .describe('Absolute path to the unpacked extension folder.'), }, handler: async (request, response, context) => { @@ -48,3 +49,29 @@ export const uninstallExtension = defineTool({ response.appendResponseLine(`Extension uninstalled. Id: ${id}`); }, }); + +export const listExtensions = defineTool({ + name: 'list_extensions', + description: + 'Lists all installed extensions, including their name, ID, version, and enabled status.', + annotations: { + category: ToolCategory.EXTENSIONS, + readOnlyHint: true, + conditions: [EXTENSIONS_CONDITION], + }, + schema: {}, + handler: async (request, response, context) => { + const extensions = context.listExtensions(); + if (extensions.length === 0) { + response.appendResponseLine('No extensions installed.'); + return; + } + const table = extensions.map(ext => ({ + Name: ext.name, + ID: ext.id, + Version: ext.version, + Enabled: ext.isEnabled ? 'Yes' : 'No', + })); + response.appendResponseLine(JSON.stringify(table, null, 2)); + }, +}); diff --git a/src/utils/ExtensionRegistry.ts b/src/utils/ExtensionRegistry.ts new file mode 100644 index 000000000..0a282f57e --- /dev/null +++ b/src/utils/ExtensionRegistry.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'node:fs/promises'; +import path from 'node:path'; + +export interface InstalledExtension { + id: string; + name: string; + version: string; + isEnabled: boolean; + path: string; +} + +export class ExtensionRegistry { + #extensions = new Map(); + + async registerExtension(id: string, extensionPath: string): Promise { + const manifestPath = path.join(extensionPath, 'manifest.json'); + const manifestContent = await fs.readFile(manifestPath, 'utf-8'); + const manifest = JSON.parse(manifestContent); + const name = manifest.name ?? 'Unknown'; + const version = manifest.version ?? 'Unknown'; + + const extension: InstalledExtension = { + id, + name, + version, + isEnabled: true, + path: extensionPath, + }; + this.add(extension); + return extension; + } + + add(extension: InstalledExtension): void { + this.#extensions.set(extension.id, extension); + } + + remove(id: string): void { + this.#extensions.delete(id); + } + + list(): InstalledExtension[] { + return Array.from(this.#extensions.values()); + } + + getById(id: string): InstalledExtension | undefined { + return this.#extensions.get(id); + } +} + diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index effaf4807..4da72281f 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -11,6 +11,7 @@ import {describe, it} from 'node:test'; import { installExtension, uninstallExtension, + listExtensions, } from '../../src/tools/extensions.js'; import {withMcpContext} from '../utils.js'; @@ -71,4 +72,36 @@ describe('extension', () => { ); }); }); + it('lists installed extensions', async () => { + await withMcpContext(async (response, context) => { + await installExtension.handler( + { params: { path: EXTENSION_PATH } }, + response, + context, + ); + + await listExtensions.handler({ params: {} }, response, context); + + const listResponseLine = response.responseLines[1]; + assert.ok(listResponseLine, 'Response should not be empty'); + const extensions = JSON.parse(listResponseLine); + assert.strictEqual(extensions.length, 1); + assert.strictEqual(extensions[0].Name, 'Test Extension'); + assert.strictEqual(extensions[0].Version, '1.0'); + assert.strictEqual(extensions[0].Enabled, 'Yes'); + + const extensionId = extensions[0].ID; + await uninstallExtension.handler( + { params: { id: extensionId } }, + response, + context, + ); + + response.resetResponseLineForTesting(); + await listExtensions.handler({ params: {} }, response, context); + + const emptyListResponse = response.responseLines[0]; + assert.strictEqual(emptyListResponse, 'No extensions installed.'); + }); + }); }); From a21bebb1fde48824d52aacf9704cb614621a8d6b Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Fri, 23 Jan 2026 17:57:29 +0000 Subject: [PATCH 2/6] Address comments --- src/McpResponse.ts | 45 ++++++++++++++++++++++++++---- src/tools/ToolDefinition.ts | 1 + src/tools/extensions.ts | 15 ++-------- src/utils/ExtensionRegistry.ts | 8 ++---- tests/McpResponse.test.js.snapshot | 35 +++++++++++++++++++++++ tests/McpResponse.test.ts | 32 +++++++++++++++++++++ tests/tools/extensions.test.ts | 31 +++----------------- 7 files changed, 115 insertions(+), 52 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index d4f538a62..05fc283d7 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -25,6 +25,7 @@ import type { } from './tools/ToolDefinition.js'; import type {InsightName, TraceResult} from './trace-processing/parse.js'; import {getInsightOutput, getTraceSummary} from './trace-processing/parse.js'; +import type {InstalledExtension} from './utils/ExtensionRegistry.js'; import {paginate} from './utils/pagination.js'; import type {PaginationOptions} from './utils/types.js'; @@ -60,6 +61,7 @@ export class McpResponse implements Response { types?: string[]; includePreservedMessages?: boolean; }; + #listExtensions?: boolean; #devToolsData?: DevToolsData; #tabId?: string; @@ -81,6 +83,10 @@ export class McpResponse implements Response { }; } + setListExtensions(): void { + this.#listExtensions = true; + } + setIncludeNetworkRequests( value: boolean, options?: PaginationOptions & { @@ -99,9 +105,9 @@ export class McpResponse implements Response { pagination: options?.pageSize || options?.pageIdx ? { - pageSize: options.pageSize, - pageIdx: options.pageIdx, - } + pageSize: options.pageSize, + pageIdx: options.pageIdx, + } : undefined, resourceTypes: options?.resourceTypes, includePreservedRequests: options?.includePreservedRequests, @@ -126,9 +132,9 @@ export class McpResponse implements Response { pagination: options?.pageSize || options?.pageIdx ? { - pageSize: options.pageSize, - pageIdx: options.pageIdx, - } + pageSize: options.pageSize, + pageIdx: options.pageIdx, + } : undefined, types: options?.types, includePreservedMessages: options?.includePreservedMessages, @@ -297,6 +303,11 @@ export class McpResponse implements Response { } } + let extensions: InstalledExtension[] | undefined; + if (this.#listExtensions) { + extensions = context.listExtensions(); + } + let consoleMessages: Array | undefined; if (this.#consoleDataOptions?.include) { let messages = context.getConsoleData( @@ -395,6 +406,7 @@ export class McpResponse implements Response { networkRequests, traceInsight: this.#attachedTraceInsight, traceSummary: this.#attachedTraceSummary, + extensions, }); } @@ -409,6 +421,7 @@ export class McpResponse implements Response { networkRequests?: NetworkFormatter[]; traceSummary?: TraceResult; traceInsight?: TraceInsightData; + extensions?: InstalledExtension[]; }, ): {content: Array; structuredContent: object} { const response = [`# ${toolName} response`]; @@ -474,6 +487,7 @@ Call ${handleDialog.name} to handle it before continuing.`); consoleMessages?: object[]; traceSummary?: string; traceInsights?: Array<{insightName: string; insightKey: string}>; + extensions?: object[]; } = {}; if (this.#tabId) { @@ -531,6 +545,25 @@ Call ${handleDialog.name} to handle it before continuing.`); data.detailedConsoleMessage.toJSONDetailed(); } + if (data.extensions) { + structuredContent.extensions = data.extensions; + response.push('## Extensions'); + if (data.extensions.length === 0) { + response.push('No extensions installed.'); + } else { + const parts = []; + for (const extension of data.extensions) { + parts.push([ + `Name: ${extension.name}`, + `ID: ${extension.id}`, + `Version: ${extension.version}`, + `Status: ${extension.isEnabled ? 'Enabled' : 'Disabled'}`, + ].join('\n')); + } + response.push(parts.join('\n\n')); + } + } + if (this.#networkRequestsOptions?.include) { let requests = context.getNetworkRequests( this.#networkRequestsOptions?.includePreservedRequests, diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 93431f917..8f1eb0b11 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -93,6 +93,7 @@ export interface Response { insightSetId: string, insightName: InsightName, ): void; + setListExtensions(): void; } /** diff --git a/src/tools/extensions.ts b/src/tools/extensions.ts index 2bfc154e3..3a75b29ed 100644 --- a/src/tools/extensions.ts +++ b/src/tools/extensions.ts @@ -60,18 +60,7 @@ export const listExtensions = defineTool({ conditions: [EXTENSIONS_CONDITION], }, schema: {}, - handler: async (request, response, context) => { - const extensions = context.listExtensions(); - if (extensions.length === 0) { - response.appendResponseLine('No extensions installed.'); - return; - } - const table = extensions.map(ext => ({ - Name: ext.name, - ID: ext.id, - Version: ext.version, - Enabled: ext.isEnabled ? 'Yes' : 'No', - })); - response.appendResponseLine(JSON.stringify(table, null, 2)); + handler: async (_request, response, _context) => { + response.setListExtensions(); }, }); diff --git a/src/utils/ExtensionRegistry.ts b/src/utils/ExtensionRegistry.ts index 0a282f57e..f0b6bfe67 100644 --- a/src/utils/ExtensionRegistry.ts +++ b/src/utils/ExtensionRegistry.ts @@ -25,19 +25,15 @@ export class ExtensionRegistry { const name = manifest.name ?? 'Unknown'; const version = manifest.version ?? 'Unknown'; - const extension: InstalledExtension = { + const extension = { id, name, version, isEnabled: true, path: extensionPath, }; - this.add(extension); - return extension; - } - - add(extension: InstalledExtension): void { this.#extensions.set(extension.id, extension); + return extension; } remove(id: string): void { diff --git a/tests/McpResponse.test.js.snapshot b/tests/McpResponse.test.js.snapshot index c2034ee7d..235b719f4 100644 --- a/tests/McpResponse.test.js.snapshot +++ b/tests/McpResponse.test.js.snapshot @@ -480,3 +480,38 @@ exports[`McpResponse network request filtering > shows no requests when filter m ## Network requests No requests found. `; + +exports[`extensions > lists extensions 1`] = ` +# test response +## Extensions +Name: Extension 1 +ID: id1 +Version: 1.0 +Status: Enabled + +Name: Extension 2 +ID: id2 +Version: 2.0 +Status: Disabled +`; + +exports[`extensions > lists extensions 2`] = ` +{ + "extensions": [ + { + "id": "id1", + "name": "Extension 1", + "version": "1.0", + "isEnabled": true, + "path": "/path/to/ext1" + }, + { + "id": "id2", + "name": "Extension 2", + "version": "2.0", + "isEnabled": false, + "path": "/path/to/ext2" + } + ] +} +`; diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 574b1309b..3d829d1b6 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -713,3 +713,35 @@ describe('McpResponse network pagination', () => { }); }); }); + +describe('extensions', () => { + it('lists extensions', async t => { + await withMcpContext(async (response, context) => { + context.listExtensions = () => [ + { + id: 'id1', + name: 'Extension 1', + version: '1.0', + isEnabled: true, + path: '/path/to/ext1', + }, + { + id: 'id2', + name: 'Extension 2', + version: '2.0', + isEnabled: false, + path: '/path/to/ext2', + }, + ]; + response.setListExtensions(); + const { content, structuredContent } = await response.handle( + 'test', + context, + ); + + t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.(JSON.stringify(structuredContent, null, 2)); + }); + }); +}); + diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index 4da72281f..2d88a28ff 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -8,6 +8,8 @@ import assert from 'node:assert'; import path from 'node:path'; import {describe, it} from 'node:test'; +import sinon from 'sinon'; + import { installExtension, uninstallExtension, @@ -74,34 +76,9 @@ describe('extension', () => { }); it('lists installed extensions', async () => { await withMcpContext(async (response, context) => { - await installExtension.handler( - { params: { path: EXTENSION_PATH } }, - response, - context, - ); - + const setListExtensionsSpy = sinon.spy(response, 'setListExtensions'); await listExtensions.handler({ params: {} }, response, context); - - const listResponseLine = response.responseLines[1]; - assert.ok(listResponseLine, 'Response should not be empty'); - const extensions = JSON.parse(listResponseLine); - assert.strictEqual(extensions.length, 1); - assert.strictEqual(extensions[0].Name, 'Test Extension'); - assert.strictEqual(extensions[0].Version, '1.0'); - assert.strictEqual(extensions[0].Enabled, 'Yes'); - - const extensionId = extensions[0].ID; - await uninstallExtension.handler( - { params: { id: extensionId } }, - response, - context, - ); - - response.resetResponseLineForTesting(); - await listExtensions.handler({ params: {} }, response, context); - - const emptyListResponse = response.responseLines[0]; - assert.strictEqual(emptyListResponse, 'No extensions installed.'); + assert.ok(setListExtensionsSpy.calledOnce, 'setListExtensions should be called'); }); }); }); From 4827b34502b04c4b1cded3c7640fc017c47435b5 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Fri, 23 Jan 2026 18:03:37 +0000 Subject: [PATCH 3/6] formatting --- src/McpContext.ts | 6 ++++-- src/McpResponse.ts | 26 ++++++++++++++------------ src/tools/ToolDefinition.ts | 2 +- src/utils/ExtensionRegistry.ts | 6 ++++-- tests/McpResponse.test.ts | 3 +-- tests/tools/extensions.test.ts | 7 +++++-- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 98f223d4d..7335a1308 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -35,7 +35,10 @@ import {takeSnapshot} from './tools/snapshot.js'; import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js'; import type {Context, DevToolsData} from './tools/ToolDefinition.js'; import type {TraceResult} from './trace-processing/parse.js'; -import { ExtensionRegistry, type InstalledExtension } from './utils/ExtensionRegistry.js'; +import { + ExtensionRegistry, + type InstalledExtension, +} from './utils/ExtensionRegistry.js'; import {WaitForHelper} from './WaitForHelper.js'; export interface TextSnapshotNode extends SerializedAXNode { @@ -770,4 +773,3 @@ export class McpContext implements Context { return this.#extensionRegistry.list(); } } - diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 05fc283d7..acce6618c 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -105,9 +105,9 @@ export class McpResponse implements Response { pagination: options?.pageSize || options?.pageIdx ? { - pageSize: options.pageSize, - pageIdx: options.pageIdx, - } + pageSize: options.pageSize, + pageIdx: options.pageIdx, + } : undefined, resourceTypes: options?.resourceTypes, includePreservedRequests: options?.includePreservedRequests, @@ -132,9 +132,9 @@ export class McpResponse implements Response { pagination: options?.pageSize || options?.pageIdx ? { - pageSize: options.pageSize, - pageIdx: options.pageIdx, - } + pageSize: options.pageSize, + pageIdx: options.pageIdx, + } : undefined, types: options?.types, includePreservedMessages: options?.includePreservedMessages, @@ -553,12 +553,14 @@ Call ${handleDialog.name} to handle it before continuing.`); } else { const parts = []; for (const extension of data.extensions) { - parts.push([ - `Name: ${extension.name}`, - `ID: ${extension.id}`, - `Version: ${extension.version}`, - `Status: ${extension.isEnabled ? 'Enabled' : 'Disabled'}`, - ].join('\n')); + parts.push( + [ + `Name: ${extension.name}`, + `ID: ${extension.id}`, + `Version: ${extension.version}`, + `Status: ${extension.isEnabled ? 'Enabled' : 'Disabled'}`, + ].join('\n'), + ); } response.push(parts.join('\n\n')); } diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 8f1eb0b11..11b01a44b 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -13,7 +13,7 @@ import type { Viewport, } from '../third_party/index.js'; import type {InsightName, TraceResult} from '../trace-processing/parse.js'; -import type { InstalledExtension } from '../utils/ExtensionRegistry.js'; +import type {InstalledExtension} from '../utils/ExtensionRegistry.js'; import type {PaginationOptions} from '../utils/types.js'; import type {ToolCategory} from './categories.js'; diff --git a/src/utils/ExtensionRegistry.ts b/src/utils/ExtensionRegistry.ts index f0b6bfe67..e652d64fe 100644 --- a/src/utils/ExtensionRegistry.ts +++ b/src/utils/ExtensionRegistry.ts @@ -18,7 +18,10 @@ export interface InstalledExtension { export class ExtensionRegistry { #extensions = new Map(); - async registerExtension(id: string, extensionPath: string): Promise { + async registerExtension( + id: string, + extensionPath: string, + ): Promise { const manifestPath = path.join(extensionPath, 'manifest.json'); const manifestContent = await fs.readFile(manifestPath, 'utf-8'); const manifest = JSON.parse(manifestContent); @@ -48,4 +51,3 @@ export class ExtensionRegistry { return this.#extensions.get(id); } } - diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 3d829d1b6..256a5e731 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -734,7 +734,7 @@ describe('extensions', () => { }, ]; response.setListExtensions(); - const { content, structuredContent } = await response.handle( + const {content, structuredContent} = await response.handle( 'test', context, ); @@ -744,4 +744,3 @@ describe('extensions', () => { }); }); }); - diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index 2d88a28ff..0405c5f8b 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -77,8 +77,11 @@ describe('extension', () => { it('lists installed extensions', async () => { await withMcpContext(async (response, context) => { const setListExtensionsSpy = sinon.spy(response, 'setListExtensions'); - await listExtensions.handler({ params: {} }, response, context); - assert.ok(setListExtensionsSpy.calledOnce, 'setListExtensions should be called'); + await listExtensions.handler({params: {}}, response, context); + assert.ok( + setListExtensionsSpy.calledOnce, + 'setListExtensions should be called', + ); }); }); }); From 12d3c3591f5fa5c849a2d6d7bc44dc083b45b3a3 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 26 Jan 2026 10:19:13 +0000 Subject: [PATCH 4/6] Comments fix: extensions response format --- src/McpResponse.ts | 16 ++++------------ src/tools/extensions.ts | 1 - tests/McpResponse.test.js.snapshot | 11 ++--------- tests/McpResponse.test.ts | 8 ++++++++ 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index acce6618c..51f58e794 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -551,18 +551,10 @@ Call ${handleDialog.name} to handle it before continuing.`); if (data.extensions.length === 0) { response.push('No extensions installed.'); } else { - const parts = []; - for (const extension of data.extensions) { - parts.push( - [ - `Name: ${extension.name}`, - `ID: ${extension.id}`, - `Version: ${extension.version}`, - `Status: ${extension.isEnabled ? 'Enabled' : 'Disabled'}`, - ].join('\n'), - ); - } - response.push(parts.join('\n\n')); + const extensionsMessage = data.extensions.map(extension => { + return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.isEnabled ? 'Enabled' : 'Disabled'}`; + }).join('\n'); + response.push(extensionsMessage); } } diff --git a/src/tools/extensions.ts b/src/tools/extensions.ts index 3a75b29ed..619b26c1f 100644 --- a/src/tools/extensions.ts +++ b/src/tools/extensions.ts @@ -22,7 +22,6 @@ export const installExtension = defineTool({ schema: { path: zod .string() - .trim() .describe('Absolute path to the unpacked extension folder.'), }, handler: async (request, response, context) => { diff --git a/tests/McpResponse.test.js.snapshot b/tests/McpResponse.test.js.snapshot index 235b719f4..6e12fff21 100644 --- a/tests/McpResponse.test.js.snapshot +++ b/tests/McpResponse.test.js.snapshot @@ -484,15 +484,8 @@ No requests found. exports[`extensions > lists extensions 1`] = ` # test response ## Extensions -Name: Extension 1 -ID: id1 -Version: 1.0 -Status: Enabled - -Name: Extension 2 -ID: id2 -Version: 2.0 -Status: Disabled +id=id1 "Extension 1" v1.0 Enabled +id=id2 "Extension 2" v2.0 Disabled `; exports[`extensions > lists extensions 2`] = ` diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 256a5e731..a482de75a 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -717,6 +717,14 @@ describe('McpResponse network pagination', () => { describe('extensions', () => { it('lists extensions', async t => { await withMcpContext(async (response, context) => { + response.setListExtensions(); + // Empty state testing + const emptyResult = await response.handle('test', context); + const emptyText = getTextContent(emptyResult.content[0]); + assert.ok(emptyText.includes('No extensions installed.'), 'Should show message for ampty extensions'); + + response.resetResponseLineForTesting(); + // Testing with extensions context.listExtensions = () => [ { id: 'id1', From 8b85e9c65e1789a4e2598d8ce708641dd5a742aa Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 26 Jan 2026 10:21:29 +0000 Subject: [PATCH 5/6] formatting --- src/McpResponse.ts | 8 +++++--- tests/McpResponse.test.ts | 7 +++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 51f58e794..7ed2e7577 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -551,9 +551,11 @@ Call ${handleDialog.name} to handle it before continuing.`); if (data.extensions.length === 0) { response.push('No extensions installed.'); } else { - const extensionsMessage = data.extensions.map(extension => { - return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.isEnabled ? 'Enabled' : 'Disabled'}`; - }).join('\n'); + const extensionsMessage = data.extensions + .map(extension => { + return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.isEnabled ? 'Enabled' : 'Disabled'}`; + }) + .join('\n'); response.push(extensionsMessage); } } diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index a482de75a..68eac2f0b 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -718,10 +718,13 @@ describe('extensions', () => { it('lists extensions', async t => { await withMcpContext(async (response, context) => { response.setListExtensions(); - // Empty state testing + // Empty state testing const emptyResult = await response.handle('test', context); const emptyText = getTextContent(emptyResult.content[0]); - assert.ok(emptyText.includes('No extensions installed.'), 'Should show message for ampty extensions'); + assert.ok( + emptyText.includes('No extensions installed.'), + 'Should show message for ampty extensions', + ); response.resetResponseLineForTesting(); // Testing with extensions From ac209ea6cafd1d93ddae7f4f3a2e5be3ec7b092e Mon Sep 17 00:00:00 2001 From: Natasha Gorshunova <47688881+nattallius@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:22:45 +0100 Subject: [PATCH 6/6] Update src/tools/extensions.ts Co-authored-by: Alex Rudenko --- src/tools/extensions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/extensions.ts b/src/tools/extensions.ts index 619b26c1f..431fb9f1b 100644 --- a/src/tools/extensions.ts +++ b/src/tools/extensions.ts @@ -52,7 +52,7 @@ export const uninstallExtension = defineTool({ export const listExtensions = defineTool({ name: 'list_extensions', description: - 'Lists all installed extensions, including their name, ID, version, and enabled status.', + 'Lists all extensions via this server, including their name, ID, version, and enabled status.', annotations: { category: ToolCategory.EXTENSIONS, readOnlyHint: true,