# Recipe: Create Master Items MCP Tool (Qlik Cloud)
A complete step-by-step recipe for adding master dimension and master measure MCP tools for **Qlik Cloud** using enigma.js with API key authentication.
<img width="608" height="1262" alt="image" src="https://github.com/user-attachments/assets/89abdc65-4d93-43e2-b523-1cfe9c3c5ba6" />
---
## Platform
| Platform | Status |
|----------|--------|
| **Qlik Cloud** | Supported (this recipe) |
| On-Premise | Not covered (different auth) |
---
## What You Will Build
Two MCP tools for Qlik Cloud:
1. **`qlik_create_master_dimension`** - Create master dimensions
2. **`qlik_create_master_measure`** - Create master measures
**Example Usage:**
```
User: "Create a master dimension called 'Region' using the Country field"
Claude: Uses qlik_create_master_dimension → Returns dimension ID
User: "Create a master measure called 'Total Sales' with Sum(Sales)"
Claude: Uses qlik_create_master_measure → Returns measure ID
```
---
## Prerequisites
- Qlik Cloud tenant with API key
- enigma.js and ws packages installed
- App ID where master items will be created
---
## Step 1: Create Tool Definitions
**File:** `src/tools/master-items-tools.ts`
```typescript
/**
* Tool definitions for master items (Cloud)
*/
export const MASTER_ITEMS_TOOLS = {
qlik_create_master_dimension: {
name: 'qlik_create_master_dimension',
description: `Create a master dimension in a Qlik Cloud app.
Master dimensions are reusable dimension definitions stored in the app's library.
**Examples:**
- Simple: { "appId": "abc-123", "name": "Region", "fieldDefs": ["Country"] }
- Drill-down: { "appId": "abc-123", "name": "Time", "fieldDefs": ["Year", "Quarter", "Month"], "grouping": "H" }
**Returns:** Master dimension ID`,
inputSchema: {
type: 'object',
properties: {
appId: {
type: 'string',
description: 'Qlik app ID'
},
name: {
type: 'string',
description: 'Name for the master dimension'
},
fieldDefs: {
type: 'array',
items: { type: 'string' },
description: 'Field(s) for the dimension. Multiple fields = drill-down.'
},
label: {
type: 'string',
description: 'Display label (defaults to name)'
},
description: {
type: 'string',
description: 'Description for the library'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags for organizing'
},
grouping: {
type: 'string',
enum: ['N', 'H', 'C'],
description: 'N=None, H=Drill-down, C=Cyclic'
}
},
required: ['appId', 'name', 'fieldDefs']
}
},
qlik_create_master_measure: {
name: 'qlik_create_master_measure',
description: `Create a master measure in a Qlik Cloud app.
Master measures are reusable calculated expressions.
**Examples:**
- Simple: { "appId": "abc-123", "name": "Total Sales", "expression": "Sum(Sales)" }
- Formatted: { "appId": "abc-123", "name": "Revenue", "expression": "Sum(Sales*Price)", "formatPattern": "$#,##0" }
**Returns:** Master measure ID`,
inputSchema: {
type: 'object',
properties: {
appId: {
type: 'string',
description: 'Qlik app ID'
},
name: {
type: 'string',
description: 'Name for the master measure'
},
expression: {
type: 'string',
description: 'Measure expression (e.g., "Sum(Sales)")'
},
label: {
type: 'string',
description: 'Display label (defaults to name)'
},
description: {
type: 'string',
description: 'Description for the library'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags for organizing'
},
formatPattern: {
type: 'string',
description: 'Number format (e.g., "#,##0.00", "$#,##0", "0.0%")'
}
},
required: ['appId', 'name', 'expression']
}
}
};
```
---
## Step 2: Create Service
**File:** `src/services/master-items-service.ts`
```typescript
/**
* Master Items Service (Qlik Cloud)
* Uses enigma.js with API key authentication
*/
import enigma from 'enigma.js';
import WebSocket from 'ws';
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
// Load enigma schema (same pattern as existing codebase)
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
let schema: any;
try {
const schemaPath = join(__dirname, '../../node_modules/enigma.js/schemas/12.1306.0.json');
schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
} catch {
// Fallback path for different directory structure
const schemaPath = join(__dirname, '../../../node_modules/enigma.js/schemas/12.1306.0.json');
schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
}
// ==================== INTERFACES ====================
export interface MasterDimensionInput {
appId: string;
name: string;
fieldDefs: string[];
label?: string;
description?: string;
tags?: string[];
grouping?: 'N' | 'H' | 'C';
}
export interface MasterMeasureInput {
appId: string;
name: string;
expression: string;
label?: string;
description?: string;
tags?: string[];
formatPattern?: string;
}
export interface MasterItemResult {
success: boolean;
itemId?: string;
itemType: 'dimension' | 'measure';
name: string;
error?: string;
}
// ==================== SERVICE ====================
export class MasterItemsService {
private tenantUrl: string;
private apiKey: string;
constructor(tenantUrl: string, apiKey: string) {
this.tenantUrl = tenantUrl;
this.apiKey = apiKey;
}
/**
* Create a master dimension
*/
async createMasterDimension(input: MasterDimensionInput): Promise<MasterItemResult> {
console.error(`[MasterItems] Creating dimension: ${input.name}`);
let session: any;
try {
const { session: s, app } = await this.openApp(input.appId);
session = s;
// Build dimension properties
const qProp = {
qInfo: { qType: 'dimension' },
qDim: {
qGrouping: input.grouping || 'N',
qFieldDefs: input.fieldDefs,
qFieldLabels: input.fieldDefs.map(() => input.label || input.name)
},
qMetaDef: {
title: input.name,
description: input.description || '',
tags: input.tags || []
}
};
// Create dimension (enigma.js uses camelCase)
const dimension = await app.createDimension(qProp);
const layout = await dimension.getLayout();
// Save the app
await app.doSave();
return {
success: true,
itemId: layout.qInfo.qId,
itemType: 'dimension',
name: input.name
};
} catch (error) {
console.error('[MasterItems] Error:', error);
return {
success: false,
itemType: 'dimension',
name: input.name,
error: error instanceof Error ? error.message : String(error)
};
} finally {
if (session) {
try { await session.close(); } catch (e) { /* ignore */ }
}
}
}
/**
* Create a master measure
*/
async createMasterMeasure(input: MasterMeasureInput): Promise<MasterItemResult> {
console.error(`[MasterItems] Creating measure: ${input.name}`);
let session: any;
try {
const { session: s, app } = await this.openApp(input.appId);
session = s;
// Remove leading = if present
const expr = input.expression.startsWith('=')
? input.expression.substring(1)
: input.expression;
// Build measure properties
const qProp: any = {
qInfo: { qType: 'measure' },
qMeasure: {
qLabel: input.label || input.name,
qDef: expr,
qGrouping: 'N'
},
qMetaDef: {
title: input.name,
description: input.description || '',
tags: input.tags || []
}
};
// Add number format if specified
if (input.formatPattern) {
qProp.qMeasure.qNumFormat = {
qType: 'F',
qnDec: 2,
qUseThou: 1,
qFmt: input.formatPattern,
qDec: '.',
qThou: ','
};
}
// Create measure (enigma.js uses camelCase)
const measure = await app.createMeasure(qProp);
const layout = await measure.getLayout();
// Save the app
await app.doSave();
return {
success: true,
itemId: layout.qInfo.qId,
itemType: 'measure',
name: input.name
};
} catch (error) {
console.error('[MasterItems] Error:', error);
return {
success: false,
itemType: 'measure',
name: input.name,
error: error instanceof Error ? error.message : String(error)
};
} finally {
if (session) {
try { await session.close(); } catch (e) { /* ignore */ }
}
}
}
/**
* Open app via enigma.js with API key auth
*/
private async openApp(appId: string): Promise<{ session: any; app: any }> {
const host = this.tenantUrl.replace('https://', '').replace('http://', '');
const url = `wss://${host}/app/${appId}`;
const session = enigma.create({
schema,
url,
createSocket: (wsUrl: string) => new WebSocket(wsUrl, {
headers: {
'Authorization': `Bearer ${this.apiKey}`
}
})
});
const global = await session.open();
const app = await global.openDoc(appId);
return { session, app };
}
}
```
---
## Step 3: Create Handlers
**File:** `src/handlers/master-items-handlers.ts`
```typescript
/**
* Master Items Handlers (Cloud)
*/
import { ApiClient } from '../utils/api-client.js';
import { CacheManager } from '../utils/cache-manager.js';
import { MasterItemsService } from '../services/master-items-service.js';
/**
* Handler for qlik_create_master_dimension
*/
export async function handleCreateMasterDimension(
apiClient: ApiClient,
cacheManager: CacheManager,
args: any,
platform: 'cloud' | 'on-premise' = 'cloud',
tenantUrl: string = ''
): Promise<{ content: Array<{ type: string; text: string }> }> {
console.error(`[MasterItemsHandler] create_master_dimension (platform: ${platform})`);
// Cloud-only gate
if (platform === 'on-premise') {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: 'Master items creation via MCP is only available on Qlik Cloud',
platform: 'on-premise'
}, null, 2)
}]
};
}
try {
// Validate required params
if (!args.appId) throw new Error('appId is required');
if (!args.name) throw new Error('name is required');
if (!args.fieldDefs || args.fieldDefs.length === 0) {
throw new Error('fieldDefs is required (at least one field)');
}
// Get API key from environment
const apiKey = process.env.QLIK_API_KEY;
if (!apiKey) throw new Error('QLIK_API_KEY environment variable not set');
const service = new MasterItemsService(tenantUrl, apiKey);
const result = await service.createMasterDimension({
appId: args.appId,
name: args.name,
fieldDefs: args.fieldDefs,
label: args.label,
description: args.description,
tags: args.tags,
grouping: args.grouping
});
return {
content: [{
type: 'text',
text: JSON.stringify({
success: result.success,
dimensionId: result.itemId,
name: result.name,
type: 'Master Dimension',
fields: args.fieldDefs,
grouping: args.grouping === 'H' ? 'Drill-down' : args.grouping === 'C' ? 'Cyclic' : 'Single',
error: result.error
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error instanceof Error ? error.message : String(error)
}, null, 2)
}]
};
}
}
/**
* Handler for qlik_create_master_measure
*/
export async function handleCreateMasterMeasure(
apiClient: ApiClient,
cacheManager: CacheManager,
args: any,
platform: 'cloud' | 'on-premise' = 'cloud',
tenantUrl: string = ''
): Promise<{ content: Array<{ type: string; text: string }> }> {
console.error(`[MasterItemsHandler] create_master_measure (platform: ${platform})`);
// Cloud-only gate
if (platform === 'on-premise') {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: 'Master items creation via MCP is only available on Qlik Cloud',
platform: 'on-premise'
}, null, 2)
}]
};
}
try {
// Validate required params
if (!args.appId) throw new Error('appId is required');
if (!args.name) throw new Error('name is required');
if (!args.expression) throw new Error('expression is required');
// Get API key from environment
const apiKey = process.env.QLIK_API_KEY;
if (!apiKey) throw new Error('QLIK_API_KEY environment variable not set');
const service = new MasterItemsService(tenantUrl, apiKey);
const result = await service.createMasterMeasure({
appId: args.appId,
name: args.name,
expression: args.expression,
label: args.label,
description: args.description,
tags: args.tags,
formatPattern: args.formatPattern
});
return {
content: [{
type: 'text',
text: JSON.stringify({
success: result.success,
measureId: result.itemId,
name: result.name,
type: 'Master Measure',
expression: args.expression,
format: args.formatPattern || 'Auto',
error: result.error
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error instanceof Error ? error.message : String(error)
}, null, 2)
}]
};
}
}
```
---
## Step 4: Register Tools
**File:** `src/tools/index.ts`
```typescript
// Add import
import { MASTER_ITEMS_TOOLS } from './master-items-tools.js';
// Add to exports
export { MASTER_ITEMS_TOOLS };
// In createToolDefinitions(), add:
tools.push({
name: MASTER_ITEMS_TOOLS.qlik_create_master_dimension.name,
description: MASTER_ITEMS_TOOLS.qlik_create_master_dimension.description,
inputSchema: MASTER_ITEMS_TOOLS.qlik_create_master_dimension.inputSchema,
handler: async (args) => await router.route('qlik_create_master_dimension', args),
});
tools.push({
name: MASTER_ITEMS_TOOLS.qlik_create_master_measure.name,
description: MASTER_ITEMS_TOOLS.qlik_create_master_measure.description,
inputSchema: MASTER_ITEMS_TOOLS.qlik_create_master_measure.inputSchema,
handler: async (args) => await router.route('qlik_create_master_measure', args),
});
```
---
## Step 5: Add Router Cases
**File:** `src/server/handler-router.ts`
```typescript
// Add import
import * as masterItemsHandlers from '../handlers/master-items-handlers.js';
// Add cases in route() switch:
case 'qlik_create_master_dimension':
return await this.handleMasterItemsTool('handleCreateMasterDimension', args);
case 'qlik_create_master_measure':
return await this.handleMasterItemsTool('handleCreateMasterMeasure', args);
// Add handler method:
private async handleMasterItemsTool(handlerName: string, args: any): Promise<any> {
const handler = (masterItemsHandlers as any)[handlerName];
if (!handler) {
throw new Error(`Master items handler not found: ${handlerName}`);
}
return await handler(
this.apiClient,
this.cacheManager,
args,
this.platform,
this.tenantUrl
);
}
```
---
## Step 6: Export Handlers
**File:** `src/handlers/index.ts`
```typescript
export * from './master-items-handlers.js';
```
---
## Step 7: Build and Test
```bash
npm run build
```
**Test prompts:**
```
"Create a master dimension called 'Product Category' using the Category field in app abc-123"
"Create a master measure called 'Total Revenue' with Sum(Sales*Price) formatted as $#,##0"
"Add a drill-down dimension for Year > Quarter > Month"
```
---
## Files Summary
| File | Action |
|------|--------|
| `src/tools/master-items-tools.ts` | Create |
| `src/services/master-items-service.ts` | Create |
| `src/handlers/master-items-handlers.ts` | Create |
| `src/tools/index.ts` | Modify |
| `src/server/handler-router.ts` | Modify |
| `src/handlers/index.ts` | Modify |
---
## How It Works
1. **Authentication**: API key passed as `Authorization: Bearer` header in WebSocket connection
2. **Connection**: enigma.js connects to `wss://{tenant}/app/{appId}`
3. **Methods**: Uses `app.createDimension()` and `app.createMeasure()` (camelCase)
4. **Save**: Calls `app.doSave()` to persist changes
---
## References
- [enigma.js overview](https://qlik.dev/toolkits/enigma-js/)
- [Generate API key](https://qlik.dev/authenticate/api-key/generate-your-first-api-key/)
- [QIX CreateDimension](https://qlik.dev/apis/json-rpc/qix/doc/#createdimension)
- [QIX CreateMeasure](https://qlik.dev/apis/json-rpc/qix/doc/#createmeasure)