diff --git a/package.json b/package.json
index c3684f33f0..a164d5514f 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
     "mdast-util-to-hast": "^13.0.0",
     "normalize.css": "^8.0.1",
     "phaser": "~3.87.0",
+    "plugin-directory": "https://github.com/source-academy/plugin-directory.git",
     "query-string": "^9.0.0",
     "re-resizable": "^6.9.9",
     "react": "^18.3.1",
diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
index 28217b184e..5cd66f9812 100644
--- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
+++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
@@ -500,7 +500,7 @@ export function* evalCodeConductorSaga(
     createConductor,
     url,
     async (fileName: string) => files[fileName],
-    (pluginName: string) => {} // TODO: implement dynamic plugin loading
+    _tab => 0
   );
   const stdoutTask = yield fork(handleStdout, hostPlugin, workspaceLocation);
   yield call([hostPlugin, 'startEvaluator'], entrypointFilePath);
diff --git a/src/features/conductor/BrowserHostPlugin.ts b/src/features/conductor/BrowserHostPlugin.ts
index 80726a865e..c0c7e4ad5a 100644
--- a/src/features/conductor/BrowserHostPlugin.ts
+++ b/src/features/conductor/BrowserHostPlugin.ts
@@ -1,26 +1,32 @@
 import { BasicHostPlugin } from 'conductor/dist/conductor/host';
 import { IChannel, IConduit } from 'conductor/dist/conduit';
+import { plugins, PluginType } from 'plugin-directory';
+import type React from 'react';
+import { TabDefinition } from './TabDefinition';
 
 export class BrowserHostPlugin extends BasicHostPlugin {
   requestFile(fileName: string): Promise<string | undefined> {
     return this.__onRequestFile(fileName);
   }
   requestLoadPlugin(pluginName: string): void {
-    return this.__onRequestLoadPlugin(pluginName);
+    if (pluginName in plugins) {
+      const location = plugins[pluginName as keyof typeof plugins].resolutions[PluginType.WEB];
+      if (location) this.importAndRegisterExternalPlugin(location, this.__createTab);
+    }
   }
 
   private __onRequestFile: (fileName: string) => Promise<string | undefined>;
-  private __onRequestLoadPlugin: (pluginName: string) => void;
+  private __createTab: (tabDefinition: TabDefinition, tab: React.JSX.Element) => void;
 
   static readonly channelAttach = super.channelAttach;
   constructor(
     conduit: IConduit,
     channels: IChannel<any>[],
     onRequestFile: (fileName: string) => Promise<string | undefined>,
-    onRequestLoadPlugin: (pluginName: string) => void
+    createTab: (tabDefinition: TabDefinition, tab: React.JSX.Element) => void
   ) {
     super(conduit, channels);
     this.__onRequestFile = onRequestFile;
-    this.__onRequestLoadPlugin = onRequestLoadPlugin;
+    this.__createTab = createTab;
   }
 }
diff --git a/src/features/conductor/TabDefinition.ts b/src/features/conductor/TabDefinition.ts
new file mode 100644
index 0000000000..b8e321caff
--- /dev/null
+++ b/src/features/conductor/TabDefinition.ts
@@ -0,0 +1,5 @@
+export interface TabDefinition {
+  readonly label: string;
+  readonly iconName: string;
+  readonly description?: string;
+}
diff --git a/src/features/conductor/createConductor.ts b/src/features/conductor/createConductor.ts
index 0708aa56c8..6f34e32d98 100644
--- a/src/features/conductor/createConductor.ts
+++ b/src/features/conductor/createConductor.ts
@@ -1,14 +1,15 @@
 import { Conduit, IConduit } from 'conductor/dist/conduit';
 
 import { BrowserHostPlugin } from './BrowserHostPlugin';
+import { TabDefinition } from './TabDefinition';
 
 export function createConductor(
   evaluatorPath: string,
   onRequestFile: (fileName: string) => Promise<string | undefined>,
-  onRequestLoadPlugin: (pluginName: string) => void
+  createTab: (tabDefinition: TabDefinition, tab: React.JSX.Element) => void
 ): { hostPlugin: BrowserHostPlugin; conduit: IConduit } {
   const worker = new Worker(evaluatorPath);
   const conduit = new Conduit(worker, true);
-  const hostPlugin = conduit.registerPlugin(BrowserHostPlugin, onRequestFile, onRequestLoadPlugin);
+  const hostPlugin = conduit.registerPlugin(BrowserHostPlugin, onRequestFile, createTab);
   return { hostPlugin, conduit };
 }
diff --git a/yarn.lock b/yarn.lock
index baa41cc656..21020189cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9563,6 +9563,7 @@ __metadata:
     path-browserify: "npm:^1.0.1"
     phaser: "npm:~3.87.0"
     phaser3spectorjs: "npm:^0.0.8"
+    plugin-directory: "https://github.com/source-academy/plugin-directory.git"
     prettier: "npm:^3.3.3"
     process: "npm:^0.11.10"
     query-string: "npm:^9.0.0"
@@ -14044,6 +14045,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"plugin-directory@https://github.com/source-academy/plugin-directory.git":
+  version: 0.0.1
+  resolution: "plugin-directory@https://github.com/source-academy/plugin-directory.git#commit=8fadc3a0b8f656234659b457b15a0bef223f5f88"
+  checksum: 10c0/d085195118782e35af687e20e8dc97a541ccf46bd40aae441d8b8a7e49057e7d359362ecb31056a47ab76167f835f8abadff58e555e97fe33de21b6f89fc1450
+  languageName: node
+  linkType: hard
+
 "possible-typed-array-names@npm:^1.0.0":
   version: 1.0.0
   resolution: "possible-typed-array-names@npm:1.0.0"
