Skip to content

Commit 6f04191

Browse files
author
Natallia Harshunova
committed
Implement reload_extension tool
1 parent 0610d11 commit 6f04191

File tree

4 files changed

+73
-6
lines changed

4 files changed

+73
-6
lines changed

src/McpContext.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,4 +772,8 @@ export class McpContext implements Context {
772772
listExtensions(): InstalledExtension[] {
773773
return this.#extensionRegistry.list();
774774
}
775+
776+
getExtension(id: string): InstalledExtension | undefined {
777+
return this.#extensionRegistry.getById(id);
778+
}
775779
}

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export type Context = Readonly<{
144144
installExtension(path: string): Promise<string>;
145145
uninstallExtension(id: string): Promise<void>;
146146
listExtensions(): InstalledExtension[];
147+
getExtension(id: string): InstalledExtension | undefined;
147148
}>;
148149

149150
export function defineTool<Schema extends zod.ZodRawShape>(

src/tools/extensions.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,25 @@ export const listExtensions = defineTool({
6363
response.setListExtensions();
6464
},
6565
});
66+
67+
export const reloadExtension = defineTool({
68+
name: 'reload_extension',
69+
description: 'Reloads an unpacked Chrome extension by its ID.',
70+
annotations: {
71+
category: ToolCategory.EXTENSIONS,
72+
readOnlyHint: false,
73+
conditions: [EXTENSIONS_CONDITION],
74+
},
75+
schema: {
76+
id: zod.string().describe('ID of the extension to reload.'),
77+
},
78+
handler: async (request, response, context) => {
79+
const {id} = request.params;
80+
const extension = context.getExtension(id);
81+
if (!extension) {
82+
throw new Error(`Extension with ID ${id} not found.`);
83+
}
84+
await context.installExtension(extension.path);
85+
response.appendResponseLine('Extension reloaded.');
86+
},
87+
});

tests/tools/extensions.test.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import {describe, it} from 'node:test';
1010

1111
import sinon from 'sinon';
1212

13+
import type {McpResponse} from '../../src/McpResponse.js';
1314
import {
1415
installExtension,
1516
uninstallExtension,
1617
listExtensions,
18+
reloadExtension,
1719
} from '../../src/tools/extensions.js';
1820
import {withMcpContext} from '../utils.js';
1921

@@ -22,6 +24,15 @@ const EXTENSION_PATH = path.join(
2224
'../../../tests/tools/fixtures/extension',
2325
);
2426

27+
function extractId(response: McpResponse) {
28+
const responseLine = response.responseLines[0];
29+
assert.ok(responseLine, 'Response should not be empty');
30+
const match = responseLine.match(/Extension installed\. Id: (.+)/);
31+
const extensionId = match ? match[1] : null;
32+
assert.ok(extensionId, 'Response should contain a valid key');
33+
return extensionId;
34+
}
35+
2536
describe('extension', () => {
2637
it('installs and uninstalls an extension and verifies it in chrome://extensions', async () => {
2738
await withMcpContext(async (response, context) => {
@@ -32,12 +43,7 @@ describe('extension', () => {
3243
context,
3344
);
3445

35-
const responseLine = response.responseLines[0];
36-
assert.ok(responseLine, 'Response should not be empty');
37-
const match = responseLine.match(/Extension installed\. Id: (.+)/);
38-
const extensionId = match ? match[1] : null;
39-
assert.ok(extensionId, 'Response should contain a valid key');
40-
46+
const extensionId = extractId(response);
4147
const page = context.getSelectedPage();
4248
await page.goto('chrome://extensions');
4349

@@ -84,4 +90,38 @@ describe('extension', () => {
8490
);
8591
});
8692
});
93+
it('reloads an extension', async () => {
94+
await withMcpContext(async (response, context) => {
95+
await installExtension.handler(
96+
{params: {path: EXTENSION_PATH}},
97+
response,
98+
context,
99+
);
100+
101+
const extensionId = extractId(response);
102+
const installSpy = sinon.spy(context, 'installExtension');
103+
response.resetResponseLineForTesting();
104+
105+
await reloadExtension.handler(
106+
{params: {id: extensionId!}},
107+
response,
108+
context,
109+
);
110+
assert.ok(
111+
installSpy.calledOnceWithExactly(EXTENSION_PATH),
112+
'installExtension should be called with the extension path',
113+
);
114+
115+
const reloadResponseLine = response.responseLines[0];
116+
assert.ok(
117+
reloadResponseLine.includes('Extension reloaded'),
118+
'Response should indicate reload',
119+
);
120+
121+
const list = context.listExtensions();
122+
assert.ok(list.length === 1, 'List should have only one extension');
123+
const reinstalled = list.find(e => e.id === extensionId);
124+
assert.ok(reinstalled, 'Extension should be present after reload');
125+
});
126+
});
87127
});

0 commit comments

Comments
 (0)