diff --git a/README.md b/README.md
index d5dd8c10995132fa4f0c3f20fbfedb0305410f94..91d48e5e50e9899cfa1270ff2c5522ed1e40ed4f 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ Moreover, the project contains the imlementation of `servers`, including
 
 - `servers/model` A tool for accessing coordinate and annotation data of molecular structures.
 - `servers/volume` A tool for accessing volumetric experimental data related to molecular structures.
+- `servers/plugin-state` A basic server to store Mol* Plugin states.
 
 The project also contains performance tests (`perf-tests`), `examples`, and basic proof of concept `apps` (CIF to BinaryCIF converter and JSON domain annotation to CIF converter).
 
diff --git a/package-lock.json b/package-lock.json
index 3cc465afa6de76dc269c4ad9dab41dd3bfd21d0c..9c313aac8c467562d01a37da6bac6b63406507e1 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index f6d8ef285345be56e40d3a4bc9054c8eb8a749f6..9f4a69ce977fb73ef8cd89254a9b6ad2ab27fe25 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
     "model-server": "node lib/servers/model/server.js",
     "model-server-watch": "nodemon --watch lib lib/servers/model/server.js",
     "volume-server": "node lib/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort 1336",
+    "plugin-state": "node lib/servers/plugin-state/index.js",
     "preversion": "npm run test",
     "postversion": "git push && git push --tags",
     "prepublishOnly": "npm run test && npm run build"
@@ -70,6 +71,7 @@
     "@graphql-codegen/typescript-graphql-files-modules": "^1.11.2",
     "@graphql-codegen/typescript-graphql-request": "^1.11.2",
     "@graphql-codegen/typescript-operations": "^1.11.2",
+    "@types/cors": "^2.8.6",
     "@typescript-eslint/eslint-plugin": "^2.17.0",
     "@typescript-eslint/eslint-plugin-tslint": "^2.17.0",
     "@typescript-eslint/parser": "^2.17.0",
@@ -112,6 +114,7 @@
     "argparse": "^1.0.10",
     "body-parser": "^1.19.0",
     "compression": "^1.7.4",
+    "cors": "^2.8.5",
     "express": "^4.17.1",
     "graphql": "^14.5.8",
     "immutable": "^3.8.2",
diff --git a/src/servers/model/server/api-local.ts b/src/servers/model/server/api-local.ts
index 5b27b9859040adfe01f99e842bc5663a14ebfa5d..31aca674ebe195e63df1d6dd63931d1c21a50d04 100644
--- a/src/servers/model/server/api-local.ts
+++ b/src/servers/model/server/api-local.ts
@@ -13,6 +13,7 @@ import { StructureCache } from './structure-wrapper';
 import { now } from '../../../mol-util/now';
 import { PerformanceMonitor } from '../../../mol-util/performance-monitor';
 import { QueryName } from './api';
+import { makeDir } from '../../../mol-util/make-dir';
 
 export type LocalInput = {
     input: string,
@@ -103,18 +104,4 @@ export function wrapFileToWriter(fn: string) {
     };
 
     return w;
-}
-
-function makeDir(path: string, root?: string): boolean {
-    let dirs = path.split(/\/|\\/g),
-        dir = dirs.shift();
-
-    root = (root || '') + dir + '/';
-
-    try { fs.mkdirSync(root); }
-    catch (e) {
-        if (!fs.statSync(root).isDirectory()) throw new Error(e);
-    }
-
-    return !dirs.length || makeDir(dirs.join('/'), root);
 }
\ No newline at end of file
diff --git a/src/servers/plugin-state/index.ts b/src/servers/plugin-state/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0c37c4959db652c9610e23cbd0a3729fc558a169
--- /dev/null
+++ b/src/servers/plugin-state/index.ts
@@ -0,0 +1,242 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as express from 'express'
+import * as compression from 'compression'
+import * as cors from 'cors'
+import * as bodyParser from 'body-parser'
+import * as argparse from 'argparse'
+import * as fs from 'fs'
+import * as path from 'path'
+import { makeDir } from '../../mol-util/make-dir'
+
+interface Config {
+    working_folder: string,
+    port?: string | number,
+    app_prefix: string,
+    max_states: number
+}
+
+const cmdParser = new argparse.ArgumentParser({
+    addHelp: true
+});
+cmdParser.addArgument(['--working-folder'], { help: 'Working forlder path.', required: true });
+cmdParser.addArgument(['--port'], { help: 'Server port. Altenatively use ENV variable PORT.', type: 'int', required: false });
+cmdParser.addArgument(['--app-prefix'], { help: 'Server app prefix.', defaultValue: '', required: false });
+cmdParser.addArgument(['--max-states'], { help: 'Maxinum number of states that could be saved.', defaultValue: 40, type: 'int', required: false });
+
+const Config = cmdParser.parseArgs() as Config;
+
+if (!Config.port) Config.port = process.env.port || 1339;
+
+const app = express();
+app.use(compression(<any>{ level: 6, memLevel: 9, chunkSize: 16 * 16384, filter: () => true }));
+app.use(cors({ methods: ['GET', 'PUT'] }));
+app.use(bodyParser.json({ limit: '20mb' }));
+
+type Index = { timestamp: number, id: string, name: string, description: string, isSticky?: boolean }[]
+
+function createIndex() {
+    const fn = path.join(Config.working_folder, 'index.json');
+    if (fs.existsSync(fn)) return;
+    if (!fs.existsSync(Config.working_folder)) makeDir(Config.working_folder);
+    fs.writeFileSync(fn, '[]', 'utf-8');
+}
+
+function writeIndex(index: Index) {
+    const fn = path.join(Config.working_folder, 'index.json');
+    if (!fs.existsSync(Config.working_folder)) makeDir(Config.working_folder);
+    fs.writeFileSync(fn, JSON.stringify(index, null, 2), 'utf-8');
+}
+
+function readIndex() {
+    const fn = path.join(Config.working_folder, 'index.json');
+    if (!fs.existsSync(fn)) return [];
+    return JSON.parse(fs.readFileSync(fn, 'utf-8')) as Index;
+}
+
+function validateIndex(index: Index) {
+    if (index.length > Config.max_states) {
+        const deletes: Index = [], newIndex: Index = [];
+        const toDelete = index.length - Config.max_states; 
+        
+        for (const e of index) {
+            if (!e.isSticky && deletes.length < toDelete) {
+                deletes.push(e);
+            } else {
+                newIndex.push(e);
+            }
+        }
+        // index.slice(0, index.length - 30);
+        for (const d of deletes) {
+            try {
+                fs.unlinkSync(path.join(Config.working_folder, d.id + '.json'))
+            } catch { }
+        }
+        return newIndex;
+    }
+    return index;
+}
+
+function remove(id: string) {
+    let index = readIndex();
+    let i = 0;
+    for (const e of index) {
+        if (e.id !== id) {            
+            i++;
+            continue;
+        }
+        try {
+            for (let j = i + 1; j < index.length; j++) {
+                index[j - 1] = index[j];
+            }
+            index.pop();
+            writeIndex(index);
+        } catch { }
+        try {
+            fs.unlinkSync(path.join(Config.working_folder, e.id + '.json'))
+        } catch { }
+        return;
+    }
+}
+
+function clear() {
+    let index = readIndex();
+    for (const e of index) {
+        try {
+            fs.unlinkSync(path.join(Config.working_folder, e.id + '.json'))
+        } catch { }
+    }
+    writeIndex([]);
+}
+
+export function createv4() {
+    let d = (+new Date());
+    const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+        const r = (d + Math.random()*16)%16 | 0;
+        d = Math.floor(d/16);
+        return (c==='x' ? r : (r&0x3|0x8)).toString(16);
+    });
+    return uuid.toLowerCase();
+}
+
+function mapPath(path: string) {
+    if (!Config.app_prefix) return path;
+    return `/${Config.app_prefix}/${path}`;
+}
+
+app.get(mapPath(`/get/:id`), (req, res) => {
+    const id: string = req.params.id || '';
+    console.log('Reading', id);
+    if (id.length === 0 || id.indexOf('.') >= 0 || id.indexOf('/') >= 0 || id.indexOf('\\') >= 0) {
+        res.status(404);
+        res.end();
+        return;
+    }
+
+    fs.readFile(path.join(Config.working_folder, id + '.json'), 'utf-8', (err, data) => {
+        if (err) {
+            res.status(404);
+            res.end();
+            return;
+        }
+
+        res.writeHead(200, {
+            'Content-Type': 'application/json; charset=utf-8',
+        });
+        res.write(data);
+        res.end();
+    });
+});
+
+app.get(mapPath(`/clear`), (req, res) => {
+    clear();
+    res.status(200);
+    res.end();
+});
+
+app.get(mapPath(`/remove/:id`), (req, res) => {
+    remove((req.params.id as string || '').toLowerCase());
+    res.status(200);
+    res.end();
+});
+
+app.get(mapPath(`/latest`), (req, res) => {
+    const index = readIndex();
+    const id: string = index.length > 0 ? index[index.length - 1].id : '';
+    console.log('Reading', id);
+    if (id.length === 0 || id.indexOf('.') >= 0 || id.indexOf('/') >= 0 || id.indexOf('\\') >= 0) {
+        res.status(404);
+        res.end();
+        return;
+    }
+
+    fs.readFile(path.join(Config.working_folder, id + '.json'), 'utf-8', (err, data) => {
+        if (err) {
+            res.status(404);
+            res.end();
+            return;
+        }
+
+        res.writeHead(200, {
+            'Content-Type': 'application/json; charset=utf-8',
+        });
+        res.write(data);
+        res.end();
+    });
+});
+
+app.get(mapPath(`/list`), (req, res) => {
+    const index = readIndex();
+    res.writeHead(200, {
+        'Content-Type': 'application/json; charset=utf-8',
+    });
+    res.write(JSON.stringify(index, null, 2));
+    res.end();
+});
+
+app.post(mapPath(`/set`), (req, res) => {
+    console.log('SET', req.query.name, req.query.description);
+    const index = readIndex();
+    validateIndex(index);
+
+    const name = (req.query.name as string || new Date().toUTCString()).substr(0, 50);
+    const description = (req.query.description as string || '').substr(0, 100);
+
+    index.push({ timestamp: +new Date(), id: createv4(), name, description });
+    const entry = index[index.length - 1];
+
+    const data = JSON.stringify({
+        id: entry.id,
+        name,
+        description,
+        data: req.body
+    });
+
+    fs.writeFile(path.join(Config.working_folder, entry.id + '.json'), data, { encoding: 'utf8' }, () => res.end());
+    writeIndex(index);
+});
+
+app.get(`*`, (req, res) => {
+    res.writeHead(200, {
+        'Content-Type': 'text/plain; charset=utf-8'
+    });
+    res.write(`
+GET /list
+GET /get/:id
+GET /remove/:id
+GET /latest
+POST /set?name=...&description=... [JSON data]
+`);
+    res.end();
+})
+
+createIndex();
+app.listen(Config.port);
+
+console.log(`Mol* PluginState Server`);
+console.log('');
+console.log(JSON.stringify(Config, null, 2));
\ No newline at end of file