diff --git a/package.json b/package.json
index d114fb55c4e7fd6f88bbcd99f6ba5450dd07b5c0..5d21cb7dd83fe100854134ca400c72d678804124 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,8 @@
     "watch-viewer": "webpack build/node_modules/apps/viewer/index.js -w --mode development -o build/viewer/index.js",
     "build-canvas": "webpack build/node_modules/apps/canvas/index.js --mode development -o build/canvas/index.js",
     "watch-canvas": "webpack build/node_modules/apps/canvas/index.js -w --mode development -o build/canvas/index.js",
+    "build-ms-query": "webpack build/node_modules/apps/model-server-query/index.js --mode development -o build/model-server-query/index.js",
+    "watch-ms-query": "webpack build/node_modules/apps/model-server-query/index.js -w --mode development -o build/model-server-query/index.js",
     "model-server": "node build/node_modules/servers/model/server.js",
     "model-server-watch": "nodemon --watch build/node_modules build/node_modules/servers/model/server.js"
   },
diff --git a/src/apps/model-server-query/index.html b/src/apps/model-server-query/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..4a77ef8a04b5c276c2bf0012e3628bd11a49daa7
--- /dev/null
+++ b/src/apps/model-server-query/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+        <title>Mol* ModelServer Query Builder</title>
+    </head>
+    <body>
+        <div id="app"></div>
+        <script type="text/javascript" src="./index.js"></script>
+    </body>
+</html>
\ No newline at end of file
diff --git a/src/apps/model-server-query/index.tsx b/src/apps/model-server-query/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5df577f40cd62875738e4c136687279d2b835949
--- /dev/null
+++ b/src/apps/model-server-query/index.tsx
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react'
+import * as ReactDOM from 'react-dom'
+import * as Rx from 'rxjs'
+
+import { QueryDefinition, QueryList } from '../../servers/model/server/api'
+
+import './index.html'
+
+interface State {
+    query: Rx.BehaviorSubject<QueryDefinition>,
+    id: Rx.BehaviorSubject<string>,
+    params: Rx.BehaviorSubject<any>,
+    isBinary: Rx.BehaviorSubject<boolean>,
+    models: Rx.BehaviorSubject<number[]>,
+    url: Rx.Subject<string>
+}
+
+class Root extends React.Component<{ state: State }, {  }> {
+    parseParams(str: string) {
+        try {
+            const params = JSON.parse(str);
+            this.props.state.params.next(params);
+        } catch {
+            this.props.state.params.next({});
+        }
+    }
+
+    render() {
+        return <div>
+            <div>
+                Query: <QuerySelect state={this.props.state} />
+            </div>
+            <div>
+                ID: <input type='text' onChange={t => this.props.state.id.next(t.currentTarget.value)} />
+            </div>
+            <div>
+                Params:<br/>
+                <textarea style={{height: '300px'}} cols={80} onChange={t => this.parseParams(t.currentTarget.value)} />
+            </div>
+            <div>
+                Model numbers (empty for all): <ModelNums state={this.props.state} />
+            </div>
+            <div>
+                <input type='checkbox' onChange={t => this.props.state.isBinary.next(!!t.currentTarget.checked)} /> Binary
+            </div>
+            <div>
+                Query string:
+                <QueryUrl state={this.props.state} />
+            </div>
+        </div>
+    }
+}
+
+class QuerySelect extends React.Component<{ state: State }> {
+    render() {
+        return <select onChange={s => this.props.state.query.next(QueryList[+s.currentTarget.value].definition)}>
+            { QueryList.map((q, i) => <option value={i} key={i} selected={i === 1}>{q.definition.niceName}</option>) }
+        </select>
+    }
+}
+
+class QueryUrl extends React.Component<{ state: State }, { queryString: string }> {
+    state = { queryString: '' };
+
+    componentDidMount() {
+        this.props.state.url.subscribe(url => this.setState({ queryString: url }))
+    }
+
+    render() {
+        return <input type='text' value={this.state.queryString} style={{ width: '800px' }} />
+    }
+}
+
+class ModelNums extends React.Component<{ state: State }> {
+    render() {
+        return <input type='text' defaultValue='1' style={{ width: '300px' }} onChange={t =>
+            this.props.state.models.next(t.currentTarget.value.split(',')
+                .map(v => v.trim())
+                .filter(v => !!v)
+                .map(v => +v)
+                )} />
+    }
+}
+
+const state: State = {
+    query: new Rx.BehaviorSubject(QueryList[1].definition),
+    id: new Rx.BehaviorSubject('1cbs'),
+    params: new Rx.BehaviorSubject({ }),
+    isBinary: new Rx.BehaviorSubject(false),
+    models: new Rx.BehaviorSubject<number[]>([]),
+    url: new Rx.Subject()
+}
+
+function formatUrl() {
+    const json = JSON.stringify({
+        name: state.query.value.name,
+        id: state.id.value,
+        modelNums: state.models.value.length ? state.models.value : void 0,
+        binary: state.isBinary.value,
+        params: state.params.value
+    });
+    state.url.next(encodeURIComponent(json));
+}
+
+Rx.merge(state.query, state.id, state.params, state.isBinary, state.models).subscribe(s => formatUrl());
+
+ReactDOM.render(<Root state={state} />, document.getElementById('app'));
diff --git a/src/servers/model/server/api-local.ts b/src/servers/model/server/api-local.ts
index be895be820fb3e555e2ce36e9c84364c663403d8..27c9f4250a5ce18d38391889cfe8034dbe259b2b 100644
--- a/src/servers/model/server/api-local.ts
+++ b/src/servers/model/server/api-local.ts
@@ -17,6 +17,7 @@ export type LocalInput = {
     input: string,
     output: string,
     query: string,
+    modelNums?: number[],
     params?: any
 }[];
 
@@ -27,7 +28,7 @@ export async function runLocal(input: LocalInput) {
     }
 
     for (const job of input) {
-        JobManager.add('_local_', job.input, job.query, job.params || { }, job.output);
+        JobManager.add('_local_', job.input, job.query, job.params || { }, job.modelNums, job.output);
     }
     JobManager.sort();
 
diff --git a/src/servers/model/server/api-web.ts b/src/servers/model/server/api-web.ts
index 93332c55f5dbc1d579a8b00ab28b4201044337e0..2e80b9eb09d31abf6d5bff986fbf449445229279 100644
--- a/src/servers/model/server/api-web.ts
+++ b/src/servers/model/server/api-web.ts
@@ -103,7 +103,7 @@ export function initWebApi(app: express.Express) {
         const name = args.name;
         const entryId = args.id;
         const params = args.params || { };
-        const jobId = JobManager.add('pdb', entryId, name, params);
+        const jobId = JobManager.add('pdb', entryId, name, params, args.modelNums);
         responseMap.set(jobId, res);
         if (JobManager.size === 1) processNextJob();
     });
diff --git a/src/servers/model/server/api.ts b/src/servers/model/server/api.ts
index 93504ed29051832428fcb743039640d958577003..3416436db0a5aaa26452114383fdbc13c86ed700 100644
--- a/src/servers/model/server/api.ts
+++ b/src/servers/model/server/api.ts
@@ -88,7 +88,7 @@ const QueryMap: { [id: string]: Partial<QueryDefinition> } = {
         description: 'Computes structural assembly.',
         query: () => Queries.generators.all,
         structureTransform(p, s) {
-            return StructureSymmetry.buildAssembly(s, '' + p.name).run();
+            return StructureSymmetry.buildAssembly(s, '' + (p.name || '1')).run();
         },
         params: [{
             name: 'name',
diff --git a/src/servers/model/server/jobs.ts b/src/servers/model/server/jobs.ts
index 8791d26b101074bf32ded4a5e837eff91e954af1..f2c77d77dfb86f923ca9080c8c53af7921970d06 100644
--- a/src/servers/model/server/jobs.ts
+++ b/src/servers/model/server/jobs.ts
@@ -23,11 +23,12 @@ export interface Job {
     queryDefinition: QueryDefinition,
     normalizedParams: any,
     responseFormat: ResponseFormat,
+    modelNums?: number[],
 
     outputFilename?: string
 }
 
-export function createJob(sourceId: '_local_' | string, entryId: string, queryName: string, params: any, outputFilename?: string): Job {
+export function createJob(sourceId: '_local_' | string, entryId: string, queryName: string, params: any, modelNums?: number[], outputFilename?: string): Job {
     const queryDefinition = getQueryByName(queryName);
     if (!queryDefinition) throw new Error(`Query '${queryName}' is not supported.`);
 
@@ -42,6 +43,7 @@ export function createJob(sourceId: '_local_' | string, entryId: string, queryNa
         queryDefinition,
         normalizedParams,
         responseFormat: { isBinary: !!params.binary },
+        modelNums,
         outputFilename
     };
 }
@@ -53,8 +55,8 @@ class _JobQueue {
         return this.list.count;
     }
 
-    add(sourceId: '_local_' | string, entryId: string, queryName: string, params: any, outputFilename?: string) {
-        const job = createJob(sourceId, entryId, queryName, params, outputFilename);
+    add(sourceId: '_local_' | string, entryId: string, queryName: string, params: any, modelNums?: number[], outputFilename?: string) {
+        const job = createJob(sourceId, entryId, queryName, params, modelNums, outputFilename);
         this.list.addLast(job);
         return job.id;
     }
diff --git a/src/servers/model/server/query.ts b/src/servers/model/server/query.ts
index ee5391e0c513558ba3c37c98335aae02c7c902e1..09603e183fe2dc7a19481a70f01b61749e69e5cf 100644
--- a/src/servers/model/server/query.ts
+++ b/src/servers/model/server/query.ts
@@ -40,7 +40,7 @@ export async function resolveJob(job: Job): Promise<CifWriter.Encoder<any>> {
 
     try {
         perf.start('query');
-        const sourceStructures = await resolveStructures(wrappedStructure);
+        const sourceStructures = await resolveStructures(wrappedStructure, job.modelNums);
         if (!sourceStructures.length) throw new Error('Model not available');
 
         let structures: Structure[] = sourceStructures;