# Qlik MCP Server - Developer Guide
A comprehensive guide for developers working on the Qlik MCP (Model Context Protocol) Server.
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [Project Structure](#project-structure)
- [Authentication](#authentication)
- [Data Flow](#data-flow)
- [Adding New Tools](#adding-new-tools)
- [Platform Support](#platform-support)
- [Testing](#testing)
- [Common Patterns](#common-patterns)
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ Claude Desktop / MCP Client │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ MCP Server │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Tool │ │ Handler │ │ MCP Server │ │
│ │ Registry │→ │ Router │→ │ (stdio transport) │ │
│ └─────────────┘ └──────────────┘ └────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Handlers │
│ Route tool calls to appropriate service methods │
│ Handle platform routing (Cloud vs On-Premise) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Services │
│ Business logic, API calls, data transformation │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ API Client │
│ HTTP requests (Cloud REST API, QRS API) │
│ WebSocket connections (Engine API via enigma.js) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Qlik Cloud / Qlik Sense Enterprise │
└─────────────────────────────────────────────────────────────────┘
```
---
## Project Structure
```
src/
├── index.ts # Entry point - starts MCP server
├── server/ # MCP server infrastructure
│ ├── mcp-server.ts # MCP protocol handler (stdio)
│ ├── tool-registry.ts # Registers all tools from /tools
│ ├── handler-router.ts # Routes tool calls to handlers
│ └── index.ts # Exports
│
├── tools/ # Tool definitions (MCP schema)
│ ├── index.ts # Aggregates all tools
│ ├── governance-tools.ts # health_check, get_tenant_info, etc.
│ ├── reload-tools.ts # trigger_reload, get_reload_status
│ ├── catalog-tools.ts # list_spaces_or_streams
│ ├── data-tools.ts # get_app_data_model, get_field_values
│ ├── lineage-tools.ts # get_data_lineage, analyze_impact
│ ├── app-tools.ts # generate_app (create/update apps)
│ ├── search-tools.ts # unified_search, search_apps
│ ├── alerts-tools.ts # alert_list, alert_trigger (Cloud)
│ ├── answers-tools.ts # AI assistants (Cloud)
│ ├── automl-tools.ts # ML experiments (Cloud)
│ ├── automation-tools.ts # automation_list, automation_run
│ └── misc-tools.ts # ask_question, bulk_reload
│
├── handlers/ # Tool execution logic
│ ├── index.ts # Exports all handlers
│ ├── governance-handlers.ts # Platform-aware user/tenant/license
│ ├── reload-handlers.ts # Trigger and monitor reloads
│ ├── catalog-handlers.ts # Spaces (Cloud) / Streams (On-Prem)
│ ├── data-handlers.ts # Data model, fields, selections
│ ├── lineage-handlers.ts # Data lineage (Cloud only)
│ ├── app-handlers.ts # App creation via service
│ ├── search-handlers.ts # Unified search
│ ├── alerts-handlers.ts # Data alerts (Cloud only)
│ ├── answers-handlers.ts # Qlik Answers AI (Cloud only)
│ ├── automl-handlers.ts # AutoML (Cloud only)
│ ├── automation-handlers.ts # Automations
│ ├── misc-handlers.ts # NL questions, bulk operations
│ └── context.ts # Shared handler context
│
├── services/ # Business logic layer
│ ├── app-developer-service-simple.ts # App CRUD + Engine API
│ ├── reload-service.ts # Reload orchestration
│ ├── data-catalog-service.ts # Catalog operations
│ ├── lineage-service.ts # Lineage analysis
│ ├── unified-search-service.ts # Cross-resource search
│ ├── qlik-app-service.ts # App operations
│ ├── qlik-alert-service.ts # Alert management
│ ├── qlik-answers-service.ts # AI assistants
│ ├── qlik-automl-service.ts # ML operations
│ ├── automation-service.ts # Automation runs
│ └── natural-language-service-simple.ts # NL to Qlik
│
├── utils/ # Shared utilities
│ ├── api-client.ts # HTTP/REST client for both platforms
│ ├── cache-manager.ts # In-memory caching
│ ├── logger.ts # Logging utility
│ ├── errors.ts # Custom error classes
│ ├── helpers.ts # Utility functions
│ └── oauth-enhanced-api-client.ts # OAuth flow (experimental)
│
├── config/ # Configuration
│ ├── index.ts # Config exports
│ ├── auth.ts # Authentication config
│ ├── constants.ts # App constants
│ └── qlik-config.ts # Qlik-specific config
│
├── adapters/ # Platform adapters (future)
│ ├── base-adapter.ts # Abstract adapter
│ ├── cloud-adapter.ts # Cloud-specific
│ └── onprem-adapter.ts # On-premise specific
│
└── types/ # TypeScript type definitions
├── app-developer.ts # App creation types
└── automation.ts # Automation types
```
---
## Authentication
The MCP server supports two authentication methods based on the target platform.
### Qlik Cloud Authentication (API Key)
**Environment Variables:**
```bash
QLIK_TENANT_URL=https://your-tenant.region.qlikcloud.com
QLIK_API_KEY=eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9...
```
**How to get an API Key:**
1. Go to your Qlik Cloud tenant
2. Navigate to **Management Console** → **API keys**
3. Click **Generate new key**
4. Copy the key (shown only once)
**How it works in code (`api-client.ts`):**
```typescript
// Cloud requests use Bearer token authentication
const headers = {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json'
};
// API endpoint format
const url = `${this.config.tenantUrl}/api/v1/users/me`;
```
**API Endpoints:**
- REST API: `https://{tenant}.qlikcloud.com/api/v1/...`
- WebSocket (Engine): `wss://{tenant}.qlikcloud.com/app/{appId}`
---
### Qlik Sense Enterprise (On-Premise) Authentication
On-premise uses **certificate-based authentication** for both QRS API and Engine API.
**Environment Variables:**
```bash
QLIK_TENANT_URL=https://your-qlik-server.domain.com
QLIK_CERT_PATH=/path/to/certificates/folder
QLIK_USER_DIRECTORY=INTERNAL
QLIK_USER_ID=sa_api
```
**Certificate Folder Structure:**
```
/path/to/certificates/
├── client.pem # Client certificate (required)
├── client_key.pem # Client private key (required)
└── root.pem # Root CA certificate (optional)
```
**How to Export Certificates from QMC:**
1. Open Qlik Management Console (QMC)
2. Go to **Certificates**
3. Add a new certificate export:
- Machine name: Your server hostname
- Certificate password: Leave empty for PEM format
- Include secret key: Yes
- Export file format: **PEM**
4. Export to a folder
5. You'll get `client.pem`, `client_key.pem`, and `root.pem`
**How it works in code (`api-client.ts`):**
```typescript
// QRS API uses certificate + XRF key authentication
private initializeCertificateAuth(): void {
// Generate random XRF key (16 chars)
this.qrsXrfKey = this.generateXrfKey();
// Create HTTPS agent with certificates
this.httpsAgent = new https.Agent({
cert: fs.readFileSync(path.join(certDir, 'client.pem')),
key: fs.readFileSync(path.join(certDir, 'client_key.pem')),
ca: fs.existsSync(rootCertPath) ? fs.readFileSync(rootCertPath) : undefined,
rejectUnauthorized: false
});
}
// QRS requests include X-Qlik-User and Xrf headers
const headers = {
'X-Qlik-User': `UserDirectory=${userDirectory}; UserId=${userId}`,
'X-Qlik-Xrfkey': this.qrsXrfKey,
'Content-Type': 'application/json'
};
// QRS endpoint format (note: xrfkey in query string)
const url = `${this.config.tenantUrl}/qrs/user?xrfkey=${this.qrsXrfKey}`;
```
**Engine API Authentication (WebSocket):**
```typescript
import enigma from 'enigma.js';
import WebSocket from 'ws';
const session = enigma.create({
schema,
url: `wss://${hostname}:4747/app/${appId}`,
createSocket: (url) => new WebSocket(url, {
headers: {
'X-Qlik-User': `UserDirectory=${userDirectory}; UserId=${userId}`
},
cert: fs.readFileSync(clientCertPath),
key: fs.readFileSync(clientKeyPath),
rejectUnauthorized: false
})
});
```
**API Endpoints:**
- QRS API: `https://{server}/qrs/...` (port 443)
- Engine API: `wss://{server}:4747/app/{appId}` (port 4747)
- Engine Global: `wss://{server}:4747/app/engineData` (for CreateApp, GetOdbcDsns)
---
### Platform Detection in Code
The server automatically detects the platform based on environment variables:
```typescript
// In src/index.ts
const platform = process.env.QLIK_CERT_PATH ? 'on-premise' : 'cloud';
// Platform is passed to all handlers
router.route(toolName, args); // router has platform internally
// Handlers receive platform parameter
export async function handleMyTool(
apiClient: ApiClient,
cacheManager: CacheManager,
args: any,
platform: 'cloud' | 'on-premise' = 'cloud', // ← Platform here
tenantUrl: string = ''
): Promise<...>
```
---
## Data Flow
### Tool Call Flow
```
1. Claude sends tool call → MCP Server (stdio)
2. MCP Server → Handler Router
3. Handler Router:
- Identifies tool category (governance, reload, etc.)
- Determines platform (cloud vs on-premise)
- Routes to appropriate handler function
4. Handler:
- Validates input
- Calls service methods
- Formats response
5. Service:
- Business logic
- Calls API Client
6. API Client:
- Cloud: REST API (https://{tenant}.qlikcloud.com/api/v1/...)
- On-Premise QRS: REST API (https://{server}/qrs/...)
- On-Premise Engine: WebSocket (wss://{server}:4747/app/...)
7. Response flows back up the chain
```
### Example: `qlik_health_check` Tool Call
```
Claude: "Check if Qlik is healthy"
│
▼
Tool Call: qlik_health_check {}
│
▼
handler-router.ts: route('qlik_health_check', {})
│
├─ switch case → handleGovernanceTool('handleHealthCheck', args)
│
▼
governance-handlers.ts: handleHealthCheck(apiClient, cache, args, platform, tenantUrl)
│
├─ if (platform === 'on-premise')
│ → apiClient.makeRequest('/qrs/about')
│ else
│ → apiClient.makeRequest('/api/v1/users/me')
│
▼
api-client.ts: makeRequest(endpoint)
│
├─ Cloud: Bearer token auth
│ OR
├─ On-Premise: Certificate + XRF key auth
│
▼
Qlik API Response
│
▼
Handler formats: { success: true, status: 'healthy', platform: '...' }
│
▼
Claude receives JSON response
```
---
## Adding New Tools
This section walks through adding a new tool with **real examples** from the codebase.
---
### Complete Example: Adding `qlik_get_license_info`
Let's trace how `qlik_get_license_info` was implemented - a tool that works on both Cloud and On-Premise.
---
### Step 1: Define Tool Schema
**File:** `src/tools/governance-tools.ts`
```typescript
export const GOVERNANCE_TOOLS = {
// ... other tools ...
qlik_get_license_info: {
name: 'qlik_get_license_info',
description: 'Get license information including type, allocated seats, and usage. Works on both Cloud and On-Premise (QRS license endpoint).',
inputSchema: {
type: 'object',
properties: {
includeDetails: {
type: 'boolean',
default: true,
description: 'Include detailed license breakdown by type'
}
}
}
}
};
```
**Key Points:**
- `name`: Must match exactly in router and handler
- `description`: Shown to Claude - be descriptive about what it does
- `inputSchema`: JSON Schema format for parameters
- `required`: Array of required parameter names (optional if no required params)
---
### Step 2: Register Tool in Index
**File:** `src/tools/index.ts`
```typescript
// Import the tools
import { GOVERNANCE_TOOLS } from './governance-tools.js';
// In createToolDefinitions function, add:
tools.push({
name: GOVERNANCE_TOOLS.qlik_get_license_info.name,
description: GOVERNANCE_TOOLS.qlik_get_license_info.description,
inputSchema: GOVERNANCE_TOOLS.qlik_get_license_info.inputSchema,
handler: async (args) => await router.route('qlik_get_license_info', args),
});
```
**For Cloud-Only Tools:**
```typescript
// Wrap in platform check
if (platform === 'cloud') {
tools.push({
name: LINEAGE_TOOLS.qlik_get_lineage.name,
description: LINEAGE_TOOLS.qlik_get_lineage.description,
inputSchema: LINEAGE_TOOLS.qlik_get_lineage.inputSchema,
handler: async (args) => await router.route('qlik_get_lineage', args),
cloudOnly: true, // Mark as cloud-only
});
}
```
---
### Step 3: Create Handler Function
**File:** `src/handlers/governance-handlers.ts`
```typescript
import { ApiClient } from '../utils/api-client.js';
import { CacheManager } from '../utils/cache-manager.js';
/**
* Handler for get_license_info tool
* Get license information - works on both Cloud and On-Premise
*/
export async function handleGetLicenseInfo(
apiClient: ApiClient,
cacheManager: CacheManager,
args: any,
platform: 'cloud' | 'on-premise' = 'cloud',
tenantUrl: string = ''
): Promise<{ content: Array<{ type: string; text: string }> }> {
// Always log entry point for debugging
console.error(`[GovernanceHandlers] get_license_info called (platform: ${platform})`);
try {
let licenseInfo: any;
if (platform === 'on-premise') {
// On-Premise: Get license from QRS API
const licenseResponse = await apiClient.makeRequest('/qrs/license');
licenseInfo = {
platform: 'on-premise',
serial: licenseResponse.serial,
name: licenseResponse.name,
organization: licenseResponse.organization,
product: licenseResponse.product,
numberOfCores: licenseResponse.numberOfCores,
isExpired: licenseResponse.isExpired,
expiredReason: licenseResponse.expiredReason
};
// Try to get additional info (may fail on some versions)
try {
const accessTypes = await apiClient.makeRequest('/qrs/license/accesstypeinfo');
licenseInfo.accessTypes = accessTypes;
} catch (e) {
// Access type info might not be available - that's OK
}
} else {
// Cloud: Get license overview from Cloud API
licenseInfo = await apiClient.makeRequest('/api/v1/licenses/overview');
licenseInfo.platform = 'cloud';
}
// Return success response
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
platform,
licenseInfo,
timestamp: new Date().toISOString()
}, null, 2)
}]
};
} catch (error) {
// Always log errors
console.error('[GovernanceHandlers] Error in get_license_info:', error);
// Return error response
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error instanceof Error ? error.message : String(error)
}, null, 2)
}]
};
}
}
```
**Handler Patterns:**
1. **Both Platforms (like above):**
```typescript
if (platform === 'on-premise') {
// QRS API call
const result = await apiClient.makeRequest('/qrs/endpoint');
} else {
// Cloud API call
const result = await apiClient.makeRequest('/api/v1/endpoint');
}
```
2. **Cloud-Only Tool:**
```typescript
export async function handleLineageGet(...) {
if (platform === 'on-premise') {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: 'Data lineage is only available on Qlik Cloud',
platform: 'on-premise'
}, null, 2)
}]
};
}
// Cloud-only logic here
const result = await apiClient.makeRequest('/api/v1/lineage/...');
}
```
---
### Step 4: Add Router Case
**File:** `src/server/handler-router.ts`
```typescript
// 1. Import the handlers at the top
import * as governanceHandlers from '../handlers/governance-handlers.js';
// 2. Add case in the switch statement (inside route() method)
async route(toolName: string, args: any): Promise<any> {
try {
switch (toolName) {
// ... other cases ...
// ===== GOVERNANCE TOOLS =====
case 'qlik_get_license_info':
return await this.handleGovernanceTool('handleGetLicenseInfo', args);
// ... other cases ...
}
}
}
// 3. Add or use existing handler method
private async handleGovernanceTool(handlerName: string, args: any): Promise<any> {
const handler = (governanceHandlers as any)[handlerName];
if (!handler) {
throw new Error(`Governance handler not found: ${handlerName}`);
}
// Pass all required parameters including platform and tenantUrl
const result = await handler(
this.apiClient,
this.cacheManager,
args,
this.platform, // Important: pass platform
this.tenantUrl // Important: pass tenantUrl
);
return result;
}
```
**Router Method Naming Convention:**
- `handleGovernanceTool` - for governance handlers
- `handleReloadTool` - for reload handlers
- `handleDataTool` - for data handlers
- `handleAutomlTool` - for AutoML handlers (note: capital ML)
---
### Step 5: Export Handler (if new file)
**File:** `src/handlers/index.ts`
```typescript
// If you created a new handler file, export it
export * from './governance-handlers.js';
export * from './my-new-handlers.js'; // Add new exports
```
---
### Step 6: Build and Test
```bash
# Build TypeScript
npm run build
# Verify no errors
# Check dist/ folder has updated files
# Test with Claude Desktop
# Restart Claude Desktop after rebuild
```
---
### Real Example: Cloud-Only Tool (Alerts)
**Tool Definition** (`src/tools/alerts-tools.ts`):
```typescript
export const ALERTS_TOOLS = {
qlik_alert_list: {
name: 'qlik_alert_list',
description: 'List all data alerts for the current user. Cloud only.',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
default: 100,
description: 'Maximum alerts to return'
}
}
}
}
};
```
**Handler** (`src/handlers/alerts-handlers.ts`):
```typescript
export async function handleAlertList(
apiClient: ApiClient,
cacheManager: CacheManager,
args: any,
platform: 'cloud' | 'on-premise' = 'cloud',
tenantUrl: string = ''
): Promise<{ content: Array<{ type: string; text: string }> }> {
console.error(`[AlertsHandlers] alert_list called (platform: ${platform})`);
// Cloud-only gate
if (platform === 'on-premise') {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: 'Data alerts are only available on Qlik Cloud',
platform: 'on-premise'
}, null, 2)
}]
};
}
try {
const limit = args.limit || 100;
const response = await apiClient.makeRequest(`/api/v1/data-alerts?limit=${limit}`);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
alerts: response.data || response,
count: response.data?.length || 0,
timestamp: new Date().toISOString()
}, null, 2)
}]
};
} catch (error) {
console.error('[AlertsHandlers] Error:', error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error instanceof Error ? error.message : String(error)
}, null, 2)
}]
};
}
}
```
**Registration** (`src/tools/index.ts`):
```typescript
// Cloud-only tools are wrapped in platform check
if (platform === 'cloud') {
for (const [key, tool] of Object.entries(ALERTS_TOOLS)) {
tools.push({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
handler: async (args) => await router.route(tool.name, args),
cloudOnly: true,
});
}
}
```
---
### Checklist for Adding New Tools
- [ ] **Tool Definition**: Create in `src/tools/{category}-tools.ts`
- [ ] **Tool Registration**: Add to `src/tools/index.ts`
- [ ] **Handler Function**: Create in `src/handlers/{category}-handlers.ts`
- [ ] **Handler Export**: Export from `src/handlers/index.ts` (if new file)
- [ ] **Router Case**: Add switch case in `src/server/handler-router.ts`
- [ ] **Router Method**: Create or reuse `handle{Category}Tool` method
- [ ] **Platform Support**: Handle both platforms OR add cloud-only gate
- [ ] **Logging**: Add `console.error()` for debugging
- [ ] **Build**: Run `npm run build`
- [ ] **Test**: Test in Claude Desktop
---
## Platform Support
### Platform Detection
Platform is determined by environment variables:
```typescript
// Cloud: QLIK_TENANT_URL + QLIK_API_KEY
// On-Premise: QLIK_TENANT_URL + QLIK_CERT_PATH
```
### Cloud-Only Tools
Some features only work on Qlik Cloud:
- Answers (`qlik_answers_*`) - AI assistants
- Alerts (`qlik_alert_*`) - Data alerting
- AutoML (`qlik_automl_*`) - ML experiments
- Lineage (`qlik_get_lineage`) - Data lineage
- Automations (`qlik_automation_*`) - Workflow automation
Add a gate at the start of handlers:
```typescript
if (platform === 'on-premise') {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: 'Feature X is only available on Qlik Cloud',
platform: 'on-premise'
}, null, 2)
}]
};
}
```
### API Differences
| Operation | Cloud | On-Premise |
|-----------|-------|------------|
| List Users | `GET /api/v1/users` | `GET /qrs/user` |
| Search Users | `?search=query` | `?filter=name so 'query'` |
| Create App | `POST /api/v1/apps` | Engine API: `Global.CreateApp` |
| Reload App | `POST /api/v1/reloads` | Engine API: `Doc.DoReload` |
| List Spaces | `GET /api/v1/spaces` | `GET /qrs/stream` |
### Engine API (On-Premise)
For operations requiring Engine API:
```typescript
import enigma from 'enigma.js';
import WebSocket from 'ws';
// Global operations (CreateApp, GetOdbcDsns)
const wsUrl = `wss://${hostname}:4747/app/engineData`;
// App-specific operations (SetScript, DoReload, GetConnections)
const wsUrl = `wss://${hostname}:4747/app/${appId}`;
const session = enigma.create({
schema,
url: wsUrl,
createSocket: (url) => new WebSocket(url, {
headers: { 'X-Qlik-User': `UserDirectory=${dir}; UserId=${user}` },
cert: readFileSync(clientCertPath),
key: readFileSync(clientKeyPath),
rejectUnauthorized: false
})
});
const global = await session.open();
const doc = await global.openDoc(appId);
await doc.doReload();
await doc.doSave();
await session.close();
```
---
## Testing
### Quick API Test
```bash
# Set environment variables
export QLIK_TENANT_URL=https://your-tenant.qlikcloud.com
export QLIK_API_KEY=your-api-key
# Run quick test
node test-cloud-quick.cjs
```
### Handler Integration Test
```bash
node test-mcp-handlers.mjs
```
### Manual Testing with Claude
1. Configure Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`)
2. Start conversation
3. Ask Claude to use Qlik tools
### Debug Logging
All handlers use `console.error()` for logging (appears in Claude Desktop logs):
```typescript
console.error(`[HandlerName] operation called (platform: ${platform})`);
console.error(`[HandlerName] Args: ${JSON.stringify(args)}`);
```
View logs:
- macOS: `~/Library/Logs/Claude/mcp*.log`
- Windows: `%APPDATA%\Claude\logs\mcp*.log`
---
## Common Patterns
### Handler Response Format
Always return this structure:
```typescript
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true, // or false
// ... your data
timestamp: new Date().toISOString()
}, null, 2)
}]
};
```
### Error Handling
```typescript
try {
// ... logic
} catch (error) {
console.error('[HandlerName] Error:', error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error instanceof Error ? error.message : String(error)
}, null, 2)
}]
};
}
```
### QRS Filter Operators (On-Premise)
```typescript
// Equals
`name eq 'value'`
// Substring (contains)
`name so 'value'`
// Starts with
`name sw 'value'`
// Ends with
`name ew 'value'`
// Not equals
`name ne 'value'`
// Combine with or/and
`name so 'query' or userId so 'query'`
```
### Service Constructor Pattern
```typescript
export class MyService {
constructor(
private apiClient: ApiClient,
private cacheManager: CacheManager,
private platform: 'cloud' | 'on-premise' = 'cloud',
private tenantUrl: string = ''
) {}
}
```
---
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `QLIK_TENANT_URL` | Yes | Qlik Cloud or On-Premise URL |
| `QLIK_API_KEY` | Cloud | API key for Qlik Cloud |
| `QLIK_CERT_PATH` | On-Prem | Path to certificate folder |
| `QLIK_USER_DIRECTORY` | On-Prem | User directory (default: INTERNAL) |
| `QLIK_USER_ID` | On-Prem | User ID (default: sa_api) |
---
## Troubleshooting
### "Handler not found" Error
1. Check tool name in `handler-router.ts` switch statement
2. Verify handler is exported from handlers/index.ts
3. Rebuild: `npm run build`
### Platform Routing Issues
1. Check `platform` parameter is passed through handler chain
2. Add console.error logging to trace execution
3. Verify environment variables are set correctly
### Engine API Connection Fails
1. Check certificate paths exist
2. Verify port 4747 is accessible
3. Check X-Qlik-User header format
4. Use `/app/engineData` for global operations
### Response Not Matching Expected
1. Check dist/ has latest code: `npm run build`
2. Restart Claude Desktop after rebuild
3. Add console.error logging to trace data flow
---
## Contributing
1. Fork the repository
2. Create feature branch
3. Follow existing patterns
4. Add tests if applicable
5. Submit pull request
## License
MIT