diff --git a/openbis_ng_ui/src/js/components/tools/browser/ToolBrowser.jsx b/openbis_ng_ui/src/js/components/tools/browser/ToolBrowser.jsx
index 4e4ad0ba389eba29b232fb38386d8e95ad6dcb64..f306d4f7cad2df71004085214d5ff9d7a3241419 100644
--- a/openbis_ng_ui/src/js/components/tools/browser/ToolBrowser.jsx
+++ b/openbis_ng_ui/src/js/components/tools/browser/ToolBrowser.jsx
@@ -1,9 +1,18 @@
 import React from 'react'
+import Browser from '@src/js/components/common/browser/Browser.jsx'
+import ToolBrowserController from '@src/js/components/tools/browser/ToolBrowserController.js'
 import logger from '@src/js/common/logger.js'
 
-export default class ToolBrowser extends React.Component {
+class ToolBrowser extends React.Component {
+  constructor(props) {
+    super(props)
+    this.controller = this.props.controller || new ToolBrowserController()
+  }
+
   render() {
     logger.log(logger.DEBUG, 'ToolBrowser.render')
-    return 'ToolBrowser'
+    return <Browser controller={this.controller} />
   }
 }
+
+export default ToolBrowser
diff --git a/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js b/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js
new file mode 100644
index 0000000000000000000000000000000000000000..9c9f18b2e7be7eaf7ac75dbe05849550aa824014
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js
@@ -0,0 +1,141 @@
+import openbis from '@src/js/services/openbis.js'
+import actions from '@src/js/store/actions/actions.js'
+import pages from '@src/js/common/consts/pages.js'
+import objectType from '@src/js/common/consts/objectType.js'
+import objectOperation from '@src/js/common/consts/objectOperation.js'
+import BrowserController from '@src/js/components/common/browser/BrowserController.js'
+
+export default class ToolBrowserController extends BrowserController {
+  doGetPage() {
+    return pages.TOOLS
+  }
+
+  async doLoadNodes() {
+    return Promise.all([
+      openbis.searchPlugins(
+        new openbis.PluginSearchCriteria(),
+        new openbis.PluginFetchOptions()
+      )
+    ]).then(([plugins]) => {
+      const dynamicPropertyPluginNodes = plugins
+        .getObjects()
+        .filter(
+          plugin => plugin.pluginType === openbis.PluginType.DYNAMIC_PROPERTY
+        )
+        .map(plugin => {
+          return {
+            id: `dynamicPropertyPlugin/${plugin.name}`,
+            text: plugin.name,
+            object: {
+              type: objectType.DYNAMIC_PROPERTY_PLUGIN,
+              id: plugin.name
+            },
+            canMatchFilter: true,
+            canRemove: true
+          }
+        })
+
+      const entityValidationPluginNodes = plugins
+        .getObjects()
+        .filter(
+          plugin => plugin.pluginType === openbis.PluginType.ENTITY_VALIDATION
+        )
+        .map(plugin => {
+          return {
+            id: `entityValidationPlugin/${plugin.name}`,
+            text: plugin.name,
+            object: {
+              type: objectType.ENTITY_VALIDATION_PLUGIN,
+              id: plugin.name
+            },
+            canMatchFilter: true,
+            canRemove: true
+          }
+        })
+
+      let nodes = [
+        {
+          id: 'dynamicPropertyPlugins',
+          text: 'Dynamic Property Plugins',
+          children: dynamicPropertyPluginNodes,
+          childrenType: objectType.NEW_DYNAMIC_PROPERTY_PLUGIN,
+          canAdd: true
+        },
+        {
+          id: 'entityValidationPlugins',
+          text: 'Entity Validation Plugins',
+          children: entityValidationPluginNodes,
+          childrenType: objectType.NEW_ENTITY_VALIDATION_PLUGIN,
+          canAdd: true
+        }
+      ]
+
+      return nodes
+    })
+  }
+
+  doNodeAdd(node) {
+    if (node && node.childrenType) {
+      this.context.dispatch(
+        actions.objectNew(this.getPage(), node.childrenType)
+      )
+    }
+  }
+
+  doNodeRemove(node) {
+    if (!node.object) {
+      return Promise.resolve()
+    }
+
+    const { type, id } = node.object
+    const reason = 'deleted via ng_ui'
+
+    return this._prepareRemoveOperations(type, id, reason)
+      .then(operations => {
+        const options = new openbis.SynchronousOperationExecutionOptions()
+        options.setExecuteInOrder(true)
+        return openbis.executeOperations(operations, options)
+      })
+      .then(() => {
+        this.context.dispatch(actions.objectDelete(this.getPage(), type, id))
+      })
+      .catch(error => {
+        this.context.dispatch(actions.errorChange(error))
+      })
+  }
+
+  _prepareRemoveOperations(type, id, reason) {
+    if (
+      type === objectType.DYNAMIC_PROPERTY_PLUGIN ||
+      type === objectType.ENTITY_VALIDATION_PLUGIN
+    ) {
+      return this._prepareRemovePluginOperations(id, reason)
+    } else {
+      throw new Error('Unsupported type: ' + type)
+    }
+  }
+
+  _prepareRemovePluginOperations(id, reason) {
+    const options = new openbis.PluginDeletionOptions()
+    options.setReason(reason)
+    return Promise.resolve([
+      new openbis.DeletePluginsOperation(
+        [new openbis.PluginPermId(id)],
+        options
+      )
+    ])
+  }
+
+  doGetObservedModifications() {
+    return {
+      [objectType.DYNAMIC_PROPERTY_PLUGIN]: [
+        objectOperation.CREATE,
+        objectOperation.DELETE
+      ],
+      [objectType.ENTITY_VALIDATION_PLUGIN]: [
+        objectOperation.CREATE,
+        objectOperation.DELETE
+      ]
+    }
+  }
+}
diff --git a/openbis_ng_ui/src/js/services/openbis/dto.js b/openbis_ng_ui/src/js/services/openbis/dto.js
index 7389cf14e4dd5f2baec95812a44975395dc704af..6f43292034f3b1e53db919f13123df94fb3dfa17 100644
--- a/openbis_ng_ui/src/js/services/openbis/dto.js
+++ b/openbis_ng_ui/src/js/services/openbis/dto.js
@@ -53,6 +53,8 @@ const CLASS_FULL_NAMES = [
   'as/dto/person/update/PersonUpdate',
   'as/dto/person/update/UpdatePersonsOperation',
   'as/dto/plugin/PluginType',
+  'as/dto/plugin/delete/PluginDeletionOptions',
+  'as/dto/plugin/delete/DeletePluginsOperation',
   'as/dto/plugin/fetchoptions/PluginFetchOptions',
   'as/dto/plugin/id/PluginPermId',
   'as/dto/plugin/search/PluginSearchCriteria',
diff --git a/openbis_ng_ui/srcTest/js/components/AppComponentTest.js b/openbis_ng_ui/srcTest/js/components/AppComponentTest.js
index ea692cf30a149542938cfa9e57ceeb82d2b99536..852dc0e60abc0b72e49cadbcb013349ca6fec7fd 100644
--- a/openbis_ng_ui/srcTest/js/components/AppComponentTest.js
+++ b/openbis_ng_ui/srcTest/js/components/AppComponentTest.js
@@ -25,6 +25,7 @@ export default class AppComponentTest extends ComponentTest {
     openbis.mockSearchVocabularies([])
     openbis.mockSearchPersons([])
     openbis.mockSearchGroups([])
+    openbis.mockSearchPlugins([])
   }
 
   async login(app) {
diff --git a/openbis_ng_ui/srcTest/js/services/openbis/api.js b/openbis_ng_ui/srcTest/js/services/openbis/api.js
index f33f891bb32d77e7a5203dad3a3da7d51e1ceeea..59878ffc65bc180a125653d3060c203061da5d8f 100644
--- a/openbis_ng_ui/srcTest/js/services/openbis/api.js
+++ b/openbis_ng_ui/srcTest/js/services/openbis/api.js
@@ -85,6 +85,12 @@ const mockSearchVocabularies = vocabularies => {
   searchVocabularies.mockReturnValue(Promise.resolve(searchResult))
 }
 
+const mockSearchPlugins = plugins => {
+  const searchResult = new dto.SearchResult()
+  searchResult.setObjects(plugins)
+  searchPlugins.mockReturnValue(Promise.resolve(searchResult))
+}
+
 export default {
   login,
   logout,
@@ -127,5 +133,6 @@ export default {
   mockSearchPersons,
   mockSearchSampleTypes,
   mockSearchPropertyTypes,
-  mockSearchVocabularies
+  mockSearchVocabularies,
+  mockSearchPlugins
 }
diff --git a/openbis_ng_ui/srcTest/js/services/openbis/dto.js b/openbis_ng_ui/srcTest/js/services/openbis/dto.js
index 904b252629c4395b60da553ca24b4758a49c9213..ee785c5fe1ffb8220e3e2c8ba1840ca18e411791 100644
--- a/openbis_ng_ui/srcTest/js/services/openbis/dto.js
+++ b/openbis_ng_ui/srcTest/js/services/openbis/dto.js
@@ -30,6 +30,7 @@ import DeleteAuthorizationGroupsOperation from 'as/dto/authorizationgroup/delete
 import DeleteDataSetTypesOperation from 'as/dto/dataset/delete/DeleteDataSetTypesOperation'
 import DeleteExperimentTypesOperation from 'as/dto/experiment/delete/DeleteExperimentTypesOperation'
 import DeleteMaterialTypesOperation from 'as/dto/material/delete/DeleteMaterialTypesOperation'
+import DeletePluginsOperation from 'as/dto/plugin/delete/DeletePluginsOperation'
 import DeletePropertyTypesOperation from 'as/dto/property/delete/DeletePropertyTypesOperation'
 import DeleteRoleAssignmentsOperation from 'as/dto/roleassignment/delete/DeleteRoleAssignmentsOperation'
 import DeleteSampleTypesOperation from 'as/dto/sample/delete/DeleteSampleTypesOperation'
@@ -60,6 +61,7 @@ import PersonPermId from 'as/dto/person/id/PersonPermId'
 import PersonSearchCriteria from 'as/dto/person/search/PersonSearchCriteria'
 import PersonUpdate from 'as/dto/person/update/PersonUpdate'
 import Plugin from 'as/dto/plugin/Plugin'
+import PluginDeletionOptions from 'as/dto/plugin/delete/PluginDeletionOptions'
 import PluginFetchOptions from 'as/dto/plugin/fetchoptions/PluginFetchOptions'
 import PluginPermId from 'as/dto/plugin/id/PluginPermId'
 import PluginSearchCriteria from 'as/dto/plugin/search/PluginSearchCriteria'
@@ -162,6 +164,7 @@ const dto = {
   DeleteDataSetTypesOperation,
   DeleteExperimentTypesOperation,
   DeleteMaterialTypesOperation,
+  DeletePluginsOperation,
   DeletePropertyTypesOperation,
   DeleteRoleAssignmentsOperation,
   DeleteSampleTypesOperation,
@@ -192,6 +195,7 @@ const dto = {
   PersonSearchCriteria,
   PersonUpdate,
   Plugin,
+  PluginDeletionOptions,
   PluginFetchOptions,
   PluginPermId,
   PluginSearchCriteria,