@qikdev/mcp 6.6.10 → 6.6.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3205 +1,61 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Qik Platform MCP Server - Enhanced Version with Comprehensive Documentation
3
+ * Qik Platform MCP Server
4
4
  *
5
- * This MCP server provides comprehensive integration with the Qik platform,
6
- * enabling AI assistants to interact with Qik's content management system,
7
- * user management, forms, files, and more.
8
- *
9
- * Key Features:
10
- * - Glossary-driven architecture for dynamic content type discovery
11
- * - Smart validation based on content type definitions
12
- * - Context-aware error handling and suggestions
13
- * - Dynamic tool schema generation
14
- * - Comprehensive API coverage
15
- * - Intelligent request building and field validation
16
- * - Advanced disambiguation logic for definitions vs instances
17
- * - Workflow system documentation and automation
18
- * - Comprehensive scope and permission management
19
- *
20
- * IMPORTANT QIK CONCEPTS:
21
- *
22
- * 1. DEFINITIONS vs INSTANCES:
23
- * - Definitions: Templates that define structure (e.g., "workflow definition", "content type definition")
24
- * - Instances: Actual content items created from definitions (e.g., "workflow card", "article instance")
25
- * - When user says "create a workflow" they usually mean create a workflow DEFINITION
26
- * - When user says "add Jim to workflow X" they mean create a workflow CARD instance
27
- *
28
- * 2. WORKFLOW SYSTEM:
29
- * - Workflow Definitions: Define the structure with columns, steps, automation
30
- * - Workflow Cards: Individual items that move through the workflow
31
- * - Columns: Represent stages in the workflow (e.g., "To Do", "In Progress", "Done")
32
- * - Steps: Specific positions within columns where cards can be placed
33
- * - Automation: Entry/exit/success/fail functions that run when cards move
34
- *
35
- * 3. SCOPE SYSTEM:
36
- * - Hierarchical permission structure (like folders)
37
- * - Every content item must belong to at least one scope
38
- * - Users need appropriate permissions within scopes to perform actions
39
- * - Scopes can inherit permissions from parent scopes
40
- *
41
- * 4. CONTENT TYPE SYSTEM:
42
- * - Base types: Core Qik types (article, profile, event, etc.)
43
- * - Extended types: Custom types that extend base types with additional fields
44
- * - Fields vs DefinedFields: Fields go at root level, definedFields go in data object
5
+ * MCP server for interacting with the Qik platform API.
6
+ * Provides tools for content management, user data, and more.
45
7
  */
46
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
47
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
48
10
  import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
49
- import axios from 'axios';
50
- import FormData from 'form-data';
51
- import { ConfigManager } from './config.js';
52
- import { QikDocumentationHelper, QIK_DOCUMENTATION } from './documentation.js';
53
- // Environment variables
54
- const QIK_API_URL = process.env.QIK_API_URL || 'https://api.qik.dev';
55
- const QIK_ACCESS_TOKEN = process.env.QIK_ACCESS_TOKEN;
11
+ import { tools, getToolHandler, hasToolHandler } from "./tools/index.js";
56
12
  export class QikMCPServer {
57
13
  server;
58
- axiosInstance;
59
- glossary = {}; // Full glossary with readOnly fields (for queries)
60
- aiGlossary = {}; // AI glossary without readOnly fields (for creates/updates)
61
- userSession = null;
62
- lastGlossaryUpdate = 0;
63
- GLOSSARY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
64
- serverName = 'Qik'; // Default fallback
65
14
  constructor() {
66
- if (!QIK_ACCESS_TOKEN) {
67
- throw new Error('QIK_ACCESS_TOKEN environment variable is required. Run "qik-mcp-server setup" to configure.');
68
- }
69
15
  this.server = new Server({
70
16
  name: "qik-mcp-server",
71
- version: "2.0.0",
17
+ version: "3.0.0",
72
18
  }, {
73
19
  capabilities: {
74
20
  tools: {},
75
21
  },
76
22
  });
77
- // Configure axios instance with Qik API settings
78
- this.axiosInstance = axios.create({
79
- baseURL: QIK_API_URL,
80
- headers: {
81
- 'Authorization': `Bearer ${QIK_ACCESS_TOKEN}`,
82
- 'Content-Type': 'application/json',
83
- },
84
- timeout: 30000,
85
- });
86
23
  this.setupToolHandlers();
87
- this.initializeServer();
88
24
  // Error handling
89
- this.server.onerror = (error) => this.log(`MCP Error: ${error}`);
25
+ this.server.onerror = (error) => console.error(`MCP Error: ${error}`);
90
26
  process.on('SIGINT', async () => {
91
27
  await this.server.close();
92
28
  process.exit(0);
93
29
  });
94
30
  }
95
- log(message) {
96
- // Only log in development or when explicitly enabled
97
- if (process.env.NODE_ENV !== 'production' || process.env.QIK_MCP_DEBUG === 'true') {
98
- // Use stderr to avoid interfering with MCP JSON protocol on stdout
99
- process.stderr.write(`[Qik MCP] ${message}\n`);
100
- }
101
- }
102
- async initializeServer() {
103
- try {
104
- // Load server name from config first
105
- await this.loadServerName();
106
- // Load user session
107
- await this.loadUserSession();
108
- // Then load glossary
109
- await this.loadGlossary();
110
- this.log(`Initialized with ${Object.keys(this.glossary).length} content types`);
111
- }
112
- catch (error) {
113
- this.log(`Failed to initialize server: ${this.formatError(error)}`);
114
- }
115
- }
116
- async loadServerName() {
117
- try {
118
- const configManager = new ConfigManager();
119
- const config = await configManager.loadConfig();
120
- if (config && config.serverName) {
121
- this.serverName = config.serverName;
122
- this.log(`Loaded server name: ${this.serverName}`);
123
- }
124
- else {
125
- this.log(`No server name found in config, using default: ${this.serverName}`);
126
- }
127
- }
128
- catch (error) {
129
- this.log(`Failed to load server name from config: ${this.formatError(error)}`);
130
- // Keep default fallback value
131
- }
132
- }
133
- async loadUserSession() {
134
- try {
135
- const response = await this.axiosInstance.get('/user');
136
- this.userSession = response.data.session || response.data;
137
- this.log(`Authenticated as ${this.userSession?.firstName} ${this.userSession?.lastName}`);
138
- }
139
- catch (error) {
140
- this.log(`Failed to load user session: ${this.formatError(error)}`);
141
- }
142
- }
143
- async loadGlossary(force = false) {
144
- const now = Date.now();
145
- if (!force && this.lastGlossaryUpdate && (now - this.lastGlossaryUpdate) < this.GLOSSARY_CACHE_TTL) {
146
- return; // Use cached version
147
- }
148
- try {
149
- // Load AI-friendly glossary (which excludes readOnly fields)
150
- const aiGlossaryResponse = await this.axiosInstance.get('/glossary/ai');
151
- // Process AI-friendly glossary
152
- const newAiGlossary = (aiGlossaryResponse.data || []).reduce(function (memo, definition) {
153
- if (definition.key) {
154
- memo[definition.key] = definition;
155
- }
156
- return memo;
157
- }, {});
158
- this.aiGlossary = newAiGlossary;
159
- this.glossary = newAiGlossary; // Use AI glossary as the main glossary
160
- this.lastGlossaryUpdate = now;
161
- this.log(`Loaded ${Object.keys(this.glossary).length} content types from AI glossary`);
162
- // Log available content types for debugging
163
- const contentTypes = Object.keys(this.glossary).sort();
164
- this.log(`Available content types: ${contentTypes.join(', ')}`);
165
- }
166
- catch (error) {
167
- this.log(`Failed to load AI glossary: ${this.formatError(error)}`);
168
- // Don't clear existing glossary on error - keep what we have
169
- if (Object.keys(this.glossary).length === 0) {
170
- this.log('No cached glossary available - some operations may fail');
171
- }
172
- }
173
- }
174
- formatError(error) {
175
- if (axios.isAxiosError(error)) {
176
- const axiosError = error;
177
- if (axiosError.response) {
178
- const status = axiosError.response.status;
179
- const data = axiosError.response.data;
180
- return `HTTP ${status}: ${JSON.stringify(data)}`;
181
- }
182
- return `Network error: ${axiosError.message}`;
183
- }
184
- return error.message || String(error);
185
- }
186
- getContentTypeInfo(type) {
187
- return this.glossary[type] || null;
188
- }
189
- validateContentType(type) {
190
- const info = this.getContentTypeInfo(type);
191
- if (!info) {
192
- const availableTypes = Object.keys(this.glossary).join(', ');
193
- return {
194
- valid: false,
195
- error: `Content type '${type}' not found. Available types: ${availableTypes}`
196
- };
197
- }
198
- return { valid: true, info };
199
- }
200
- // Enhanced glossary-driven field analysis methods
201
- isFieldRequired(field) {
202
- return field.minimum !== undefined && field.minimum > 0;
203
- }
204
- isFieldArray(field) {
205
- return field.maximum === 0;
206
- }
207
- getMinimumArrayLength(field) {
208
- return this.isFieldArray(field) ? (field.minimum || 0) : 0;
209
- }
210
- /**
211
- * Validates widget-specific field values
212
- */
213
- validateWidgetValue(fieldKey, value, widgetType) {
214
- const errors = [];
215
- const warnings = [];
216
- switch (widgetType) {
217
- case 'dateobject':
218
- return this.validateDateObjectWidget(fieldKey, value);
219
- case 'date':
220
- if (typeof value === 'string') {
221
- const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
222
- if (!dateRegex.test(value)) {
223
- errors.push(`Field "${fieldKey}" with date widget must use ISO date format (YYYY-MM-DD), got: ${value}`);
224
- }
225
- }
226
- else {
227
- errors.push(`Field "${fieldKey}" with date widget must be a string in ISO date format (YYYY-MM-DD)`);
228
- }
229
- break;
230
- case 'time':
231
- if (typeof value === 'string') {
232
- const timeRegex = /^\d{2}:\d{2}(:\d{2})?$/;
233
- if (!timeRegex.test(value)) {
234
- errors.push(`Field "${fieldKey}" with time widget must use time format (HH:MM or HH:MM:SS), got: ${value}`);
235
- }
236
- }
237
- else {
238
- errors.push(`Field "${fieldKey}" with time widget must be a string in time format (HH:MM)`);
239
- }
240
- break;
241
- case 'datetime':
242
- if (typeof value === 'string') {
243
- try {
244
- new Date(value);
245
- }
246
- catch (e) {
247
- errors.push(`Field "${fieldKey}" with datetime widget must be a valid ISO datetime string, got: ${value}`);
248
- }
249
- }
250
- else {
251
- errors.push(`Field "${fieldKey}" with datetime widget must be a string in ISO datetime format`);
252
- }
253
- break;
254
- default:
255
- // No specific validation for other widget types
256
- break;
257
- }
258
- return { valid: errors.length === 0, errors, warnings };
259
- }
260
- /**
261
- * Validates dateobject widget values
262
- */
263
- validateDateObjectWidget(fieldKey, value) {
264
- const errors = [];
265
- const warnings = [];
266
- if (!value || typeof value !== 'object') {
267
- errors.push(`Field "${fieldKey}" with dateobject widget must be an object with hour, minute, day, month, year properties`);
268
- return { valid: false, errors, warnings };
269
- }
270
- const requiredProps = ['hour', 'minute', 'day', 'month', 'year'];
271
- const missingProps = requiredProps.filter(prop => value[prop] === undefined || value[prop] === null);
272
- if (missingProps.length > 0) {
273
- errors.push(`Field "${fieldKey}" dateobject is missing required properties: ${missingProps.join(', ')}`);
274
- }
275
- // Validate ranges
276
- if (typeof value.hour === 'number') {
277
- if (value.hour < 0 || value.hour > 23) {
278
- errors.push(`Field "${fieldKey}" dateobject hour must be between 0-23, got: ${value.hour}`);
279
- }
280
- }
281
- else if (value.hour !== undefined) {
282
- errors.push(`Field "${fieldKey}" dateobject hour must be a number`);
283
- }
284
- if (typeof value.minute === 'number') {
285
- if (value.minute < 0 || value.minute > 59) {
286
- errors.push(`Field "${fieldKey}" dateobject minute must be between 0-59, got: ${value.minute}`);
287
- }
288
- }
289
- else if (value.minute !== undefined) {
290
- errors.push(`Field "${fieldKey}" dateobject minute must be a number`);
291
- }
292
- if (typeof value.day === 'number') {
293
- if (value.day < 1 || value.day > 31) {
294
- errors.push(`Field "${fieldKey}" dateobject day must be between 1-31, got: ${value.day}`);
295
- }
296
- }
297
- else if (value.day !== undefined) {
298
- errors.push(`Field "${fieldKey}" dateobject day must be a number`);
299
- }
300
- if (typeof value.month === 'number') {
301
- if (value.month < 1 || value.month > 12) {
302
- errors.push(`Field "${fieldKey}" dateobject month must be between 1-12, got: ${value.month}`);
303
- }
304
- }
305
- else if (value.month !== undefined) {
306
- errors.push(`Field "${fieldKey}" dateobject month must be a number`);
307
- }
308
- if (typeof value.year === 'number') {
309
- if (value.year < 1900 || value.year > 2100) {
310
- warnings.push(`Field "${fieldKey}" dateobject year ${value.year} seems unusual (expected 1900-2100)`);
311
- }
312
- }
313
- else if (value.year !== undefined) {
314
- errors.push(`Field "${fieldKey}" dateobject year must be a number`);
315
- }
316
- return { valid: errors.length === 0, errors, warnings };
317
- }
318
- generateFieldPath(field, isDefinedField, groupPath = '') {
319
- let basePath = isDefinedField ? 'data' : '';
320
- if (groupPath) {
321
- basePath = basePath ? `${basePath}.${groupPath}` : groupPath;
322
- }
323
- return basePath ? `${basePath}.${field.key}` : field.key;
324
- }
325
- processGroupFields(fields, parentPath = '', isDefinedField = false) {
326
- const processedFields = [];
327
- for (const field of fields) {
328
- if (field.type === 'group' && field.fields) {
329
- if (field.asObject) {
330
- // Group creates nested object structure
331
- const groupPath = parentPath ? `${parentPath}.${field.key}` : field.key;
332
- processedFields.push(...this.processGroupFields(field.fields, groupPath, isDefinedField));
333
- }
334
- else {
335
- // Group is just for organization, fields remain at same level
336
- processedFields.push(...this.processGroupFields(field.fields, parentPath, isDefinedField));
337
- }
338
- }
339
- else {
340
- const fieldPath = this.generateFieldPath(field, isDefinedField, parentPath);
341
- processedFields.push({
342
- field,
343
- path: fieldPath,
344
- isRequired: this.isFieldRequired(field),
345
- isArray: this.isFieldArray(field)
346
- });
347
- }
348
- }
349
- return processedFields;
350
- }
351
- analyzeContentTypeFields(contentType) {
352
- let fields = [];
353
- let definedFields = [];
354
- // Extract fields from different possible structures
355
- if (contentType.fields) {
356
- fields = contentType.fields;
357
- definedFields = contentType.definedFields || [];
358
- }
359
- else if (contentType.definition) {
360
- fields = contentType.definition.fields || [];
361
- definedFields = contentType.definition.definedFields || [];
362
- }
363
- else if (contentType.type) {
364
- fields = contentType.type.fields || [];
365
- definedFields = contentType.type.definedFields || [];
366
- }
367
- const topLevelFields = this.processGroupFields(fields, '', false);
368
- const dataFields = this.processGroupFields(definedFields, '', true);
369
- const allFields = [...topLevelFields, ...dataFields];
370
- const allRequiredFields = allFields.filter(f => f.isRequired);
371
- return {
372
- topLevelFields,
373
- dataFields,
374
- allRequiredFields
375
- };
376
- }
377
- validateFieldData(data, fields, contentType) {
378
- const errors = [];
379
- for (const field of fields) {
380
- const value = data[field.key];
381
- // Check required fields
382
- if (field.minimum && field.minimum > 0 && (!value || (Array.isArray(value) && value.length === 0))) {
383
- errors.push(`Field '${field.key}' (${field.title}) is required for ${contentType}`);
384
- }
385
- // Type validation
386
- if (value !== undefined && value !== null) {
387
- switch (field.type) {
388
- case 'string':
389
- if (typeof value !== 'string') {
390
- errors.push(`Field '${field.key}' must be a string, got ${typeof value}`);
391
- }
392
- break;
393
- case 'number':
394
- case 'integer':
395
- if (typeof value !== 'number') {
396
- errors.push(`Field '${field.key}' must be a number, got ${typeof value}`);
397
- }
398
- break;
399
- case 'boolean':
400
- if (typeof value !== 'boolean') {
401
- errors.push(`Field '${field.key}' must be a boolean, got ${typeof value}`);
402
- }
403
- break;
404
- case 'array':
405
- if (!Array.isArray(value)) {
406
- errors.push(`Field '${field.key}' must be an array, got ${typeof value}`);
407
- }
408
- break;
409
- case 'reference':
410
- if (field.referenceType && typeof value === 'string') {
411
- // Basic validation - could be enhanced to check if referenced item exists
412
- }
413
- else if (Array.isArray(value) && field.maximum !== 1) {
414
- // Array of references
415
- }
416
- else {
417
- errors.push(`Field '${field.key}' must be a valid reference to ${field.referenceType || 'content'}`);
418
- }
419
- break;
420
- }
421
- }
422
- }
423
- return { valid: errors.length === 0, errors };
424
- }
425
- generateFieldSchema(field) {
426
- const schema = {
427
- type: this.mapFieldTypeToJsonSchema(field.type),
428
- description: field.description || field.title,
429
- };
430
- if (field.options && field.options.length > 0) {
431
- schema.enum = field.options.map(opt => opt.value);
432
- }
433
- if (field.type === 'array' && field.fields) {
434
- schema.items = {
435
- type: 'object',
436
- properties: this.generateFieldsSchema(field.fields),
437
- };
438
- }
439
- if (field.type === 'reference') {
440
- schema.description += field.referenceType ? ` (references ${field.referenceType})` : ' (content reference)';
441
- }
442
- return schema;
443
- }
444
- generateFieldsSchema(fields) {
445
- const properties = {};
446
- for (const field of fields) {
447
- properties[field.key] = this.generateFieldSchema(field);
448
- }
449
- return properties;
450
- }
451
- mapFieldTypeToJsonSchema(fieldType) {
452
- switch (fieldType) {
453
- case 'string':
454
- case 'email':
455
- case 'url':
456
- case 'date':
457
- return 'string';
458
- case 'number':
459
- case 'integer':
460
- return 'number';
461
- case 'boolean':
462
- return 'boolean';
463
- case 'array':
464
- return 'array';
465
- case 'group':
466
- case 'reference':
467
- return 'object';
468
- default:
469
- return 'string';
470
- }
471
- }
472
- /**
473
- * Generates completely dynamic properties based on the loaded glossary
474
- * This creates schema properties for ALL fields from ALL content types
475
- */
476
- generateDynamicContentProperties() {
477
- const properties = {};
478
- // Collect all unique fields from all content types in the glossary
479
- const allRootFields = new Set();
480
- const allDataFields = new Set();
481
- const fieldSchemas = new Map();
482
- // Process each content type in the glossary
483
- for (const [contentTypeKey, contentType] of Object.entries(this.glossary)) {
484
- if (!contentType || typeof contentType !== 'object')
485
- continue;
486
- // Get fields from the content type (these go at root level)
487
- const fields = contentType.fields || [];
488
- for (const field of fields) {
489
- if (field.key && field.key !== '_id') { // Skip internal ID field
490
- allRootFields.add(field.key);
491
- // Generate schema for this field
492
- if (!fieldSchemas.has(field.key)) {
493
- fieldSchemas.set(field.key, this.generateFieldSchema(field));
494
- }
495
- }
496
- }
497
- // Get definedFields from the content type (these go in data object)
498
- const definedFields = contentType.definedFields || [];
499
- for (const field of definedFields) {
500
- if (field.key) {
501
- allDataFields.add(field.key);
502
- // Generate schema for this field
503
- if (!fieldSchemas.has(field.key)) {
504
- fieldSchemas.set(field.key, this.generateFieldSchema(field));
505
- }
506
- }
507
- }
508
- }
509
- // Add all root-level fields as properties
510
- for (const fieldKey of allRootFields) {
511
- if (fieldKey !== 'title' && fieldKey !== 'meta') { // These are handled separately
512
- const schema = fieldSchemas.get(fieldKey);
513
- if (schema) {
514
- properties[fieldKey] = {
515
- ...schema,
516
- description: `${schema.description || fieldKey} (ROOT LEVEL field from glossary)`
517
- };
518
- }
519
- }
520
- }
521
- // Create comprehensive data object with all possible data fields
522
- const dataProperties = {};
523
- for (const fieldKey of allDataFields) {
524
- const schema = fieldSchemas.get(fieldKey);
525
- if (schema) {
526
- dataProperties[fieldKey] = {
527
- ...schema,
528
- description: `${schema.description || fieldKey} (DATA OBJECT field from glossary)`
529
- };
530
- }
531
- }
532
- // Add the data object with dynamic properties
533
- properties.data = {
534
- type: 'object',
535
- description: `DYNAMIC FIELD PLACEMENT (based on glossary):
536
-
537
- **ROOT LEVEL FIELDS (from glossary "fields" array):**
538
- ${Array.from(allRootFields).sort().join(', ')}
539
-
540
- **DATA OBJECT FIELDS (from glossary "definedFields" array):**
541
- ${Array.from(allDataFields).sort().join(', ')}
542
-
543
- **FIELD PLACEMENT RULE:**
544
- - Fields in glossary "fields" array → ROOT LEVEL
545
- - Fields in glossary "definedFields" array → DATA OBJECT
546
-
547
- The MCP server automatically determines correct placement based on the glossary definition for each content type.`,
548
- properties: dataProperties
549
- };
550
- return properties;
551
- }
552
- findContentTypesByDescription(description) {
553
- const normalizedDescription = description.toLowerCase().trim();
554
- const matches = [];
555
- // Direct match first (check if the description exactly matches a key)
556
- if (this.glossary[normalizedDescription]) {
557
- return [normalizedDescription];
558
- }
559
- // Search through glossary for matches
560
- for (const [key, contentType] of Object.entries(this.glossary)) {
561
- if (!contentType || typeof contentType !== 'object')
562
- continue;
563
- // Get title and plural - the glossary structure from the screenshot shows these are direct properties
564
- let title = '';
565
- let plural = '';
566
- // Based on the screenshot, the structure is directly on the contentType object
567
- if (contentType.title) {
568
- title = (contentType.title || '').toLowerCase();
569
- plural = (contentType.plural || '').toLowerCase();
570
- }
571
- // Fallback to nested structures if they exist
572
- else if (contentType.definition) {
573
- title = (contentType.definition.title || '').toLowerCase();
574
- plural = (contentType.definition.plural || '').toLowerCase();
575
- }
576
- else if (contentType.type) {
577
- title = (contentType.type.title || '').toLowerCase();
578
- plural = (contentType.type.plural || '').toLowerCase();
579
- }
580
- // Exact title/plural match (e.g., "incident report" matches "Incident Report")
581
- if (title === normalizedDescription || plural === normalizedDescription) {
582
- matches.push(key);
583
- continue;
584
- }
585
- // Partial word matches - check if all words in description are found in title
586
- const descriptionWords = normalizedDescription.split(/\s+/);
587
- const titleWords = title.split(/\s+/);
588
- if (descriptionWords.length > 0 && descriptionWords.every(word => titleWords.some(titleWord => titleWord.includes(word) || word.includes(titleWord)))) {
589
- matches.push(key);
590
- continue;
591
- }
592
- // Also check against the key itself for partial matches
593
- const keyLower = key.toLowerCase();
594
- if (descriptionWords.every(word => keyLower.includes(word))) {
595
- matches.push(key);
596
- continue;
597
- }
598
- // Specific common term matches
599
- if (normalizedDescription.includes('incident') && (title.includes('incident') || keyLower.includes('incident'))) {
600
- matches.push(key);
601
- }
602
- else if (normalizedDescription.includes('report') && (title.includes('report') || keyLower.includes('report'))) {
603
- matches.push(key);
604
- }
605
- else if (normalizedDescription.includes('event') && (title.includes('event') || keyLower.includes('event'))) {
606
- matches.push(key);
607
- }
608
- else if (normalizedDescription.includes('comment') && (title.includes('comment') || keyLower.includes('comment'))) {
609
- matches.push(key);
610
- }
611
- else if (normalizedDescription.includes('issue') && (title.includes('issue') || keyLower.includes('issue'))) {
612
- matches.push(key);
613
- }
614
- else if (normalizedDescription.includes('ticket') && (title.includes('ticket') || keyLower.includes('ticket'))) {
615
- matches.push(key);
616
- }
617
- else if (normalizedDescription.includes('case') && (title.includes('case') || keyLower.includes('case'))) {
618
- matches.push(key);
619
- }
620
- }
621
- return matches;
622
- }
623
- findContentTypeByDescription(description) {
624
- const matches = this.findContentTypesByDescription(description);
625
- return matches.length === 1 ? matches[0] : null;
626
- }
627
- async getAvailableScopes() {
628
- try {
629
- const response = await this.axiosInstance.get('/scope/tree');
630
- return response.data;
631
- }
632
- catch (error) {
633
- this.log(`Failed to get scopes: ${this.formatError(error)}`);
634
- return null;
635
- }
636
- }
637
- extractScopesWithPermissions(scopeTree, permission = 'create') {
638
- const scopes = [];
639
- const traverse = (node, path = '') => {
640
- if (!node)
641
- return;
642
- const currentPath = path ? `${path} > ${node.title || node.name || node._id}` : (node.title || node.name || node._id);
643
- // Check if user has create permission for this scope
644
- if (node.permissions && node.permissions[permission]) {
645
- scopes.push({
646
- id: node._id,
647
- title: node.title || node.name || node._id,
648
- path: currentPath
649
- });
650
- }
651
- // Traverse children
652
- if (node.children && Array.isArray(node.children)) {
653
- for (const child of node.children) {
654
- traverse(child, currentPath);
655
- }
656
- }
657
- };
658
- if (Array.isArray(scopeTree)) {
659
- for (const scope of scopeTree) {
660
- traverse(scope);
661
- }
662
- }
663
- else {
664
- traverse(scopeTree);
665
- }
666
- return scopes;
667
- }
668
- // Enhanced filter helper functions
669
- validateFilter(filter) {
670
- const errors = [];
671
- if (!filter || typeof filter !== 'object') {
672
- return { valid: true, errors: [] }; // Empty filter is valid
673
- }
674
- // Check if it's a filter group or condition
675
- if (filter.operator) {
676
- // Filter group validation
677
- if (!['and', 'or', 'nor'].includes(filter.operator)) {
678
- errors.push(`Invalid operator '${filter.operator}'. Must be 'and', 'or', or 'nor'.`);
679
- }
680
- if (!Array.isArray(filter.filters)) {
681
- errors.push('Filter group must have a "filters" array.');
682
- }
683
- else {
684
- // Recursively validate nested filters
685
- for (const nestedFilter of filter.filters) {
686
- const nestedValidation = this.validateFilter(nestedFilter);
687
- errors.push(...nestedValidation.errors);
688
- }
689
- }
690
- }
691
- else if (filter.key && filter.comparator) {
692
- // Filter condition validation
693
- const validComparators = [
694
- 'dateanniversary', 'anniversarybetween', 'anniversarynext', 'anniversarypast',
695
- 'datenext', 'datenotbetween', 'datenotnext', 'datebefore', 'dateafter',
696
- 'datetoday', 'datenottoday', 'datebeforetoday', 'dateaftertoday',
697
- 'datebeforenow', 'dateafternow', 'datepast', 'datenotpast',
698
- 'datesameday', 'datesamemonth', 'datesameweek', 'datesameyear',
699
- 'datebetween', 'datemonth',
700
- 'equal', 'notequal', 'in', 'notin', 'startswith', 'doesnotstartwith',
701
- 'endswith', 'doesnotendwith', 'contains', 'excludes',
702
- 'greater', 'lesser', 'greaterequal', 'lesserequal',
703
- 'notgreater', 'notlesser', 'notgreaterequal', 'notlesserequal',
704
- 'between', 'notbetween',
705
- 'valuesgreater', 'valueslesser', 'valuesgreaterequal', 'valueslesserequal',
706
- 'empty', 'notempty'
707
- ];
708
- if (!validComparators.includes(filter.comparator)) {
709
- errors.push(`Invalid comparator '${filter.comparator}'.`);
710
- }
711
- // Validate required values for specific comparators
712
- const requiresValue = ['equal', 'notequal', 'greater', 'lesser', 'greaterequal', 'lesserequal', 'contains', 'excludes', 'startswith', 'endswith', 'datebefore', 'dateafter', 'dateanniversary'];
713
- const requiresValues = ['in', 'notin'];
714
- const requiresValue2 = ['between', 'notbetween', 'anniversarybetween', 'anniversarynext', 'anniversarypast', 'datenext', 'datepast'];
715
- if (requiresValue.includes(filter.comparator) && filter.value === undefined) {
716
- errors.push(`Comparator '${filter.comparator}' requires a 'value' parameter.`);
717
- }
718
- if (requiresValues.includes(filter.comparator) && (!filter.values || !Array.isArray(filter.values))) {
719
- errors.push(`Comparator '${filter.comparator}' requires a 'values' array parameter.`);
720
- }
721
- if (requiresValue2.includes(filter.comparator) && filter.value2 === undefined) {
722
- errors.push(`Comparator '${filter.comparator}' requires both 'value' and 'value2' parameters.`);
723
- }
724
- }
725
- else {
726
- errors.push('Filter must be either a filter group (with operator and filters) or a condition (with key and comparator).');
727
- }
728
- return { valid: errors.length === 0, errors };
729
- }
730
- createBirthdayFilter(timeframe, amount, unit = 'days') {
731
- return {
732
- operator: 'and',
733
- filters: [{
734
- key: 'dob',
735
- comparator: timeframe === 'next' ? 'anniversarynext' : 'anniversarypast',
736
- value: amount,
737
- value2: unit
738
- }]
739
- };
740
- }
741
- createDateRangeFilter(field, startDate, endDate) {
742
- return {
743
- operator: 'and',
744
- filters: [{
745
- key: field,
746
- comparator: 'datebetween',
747
- value: startDate,
748
- value2: endDate
749
- }]
750
- };
751
- }
752
- createGenderFilter(gender) {
753
- return {
754
- key: 'gender',
755
- comparator: 'equal',
756
- value: gender
757
- };
758
- }
759
- createAgeRangeFilter(minAge, maxAge) {
760
- const currentYear = new Date().getFullYear();
761
- return {
762
- operator: 'and',
763
- filters: [{
764
- key: 'dobYear',
765
- comparator: 'between',
766
- value: currentYear - maxAge,
767
- value2: currentYear - minAge
768
- }]
769
- };
770
- }
771
- createThisMonthBirthdayFilter() {
772
- const now = new Date();
773
- const currentMonth = now.getMonth() + 1; // JavaScript months are 0-indexed
774
- return {
775
- operator: 'and',
776
- filters: [{
777
- key: 'dobMonth',
778
- comparator: 'equal',
779
- value: currentMonth
780
- }]
781
- };
782
- }
783
- createRecentContentFilter(days = 30) {
784
- return {
785
- operator: 'and',
786
- filters: [{
787
- key: 'meta.created',
788
- comparator: 'datepast',
789
- value: days,
790
- value2: 'days'
791
- }]
792
- };
793
- }
794
- createScopeFilter(scopeIds) {
795
- return {
796
- operator: 'and',
797
- filters: [{
798
- key: 'meta.scopes',
799
- comparator: 'in',
800
- values: scopeIds
801
- }]
802
- };
803
- }
804
- /**
805
- * Filters out readOnly fields from payload for create/update operations
806
- */
807
- filterReadOnlyFields(payload, contentType) {
808
- const typeInfo = this.getContentTypeInfo(contentType);
809
- if (!typeInfo) {
810
- return payload; // If we can't get type info, return payload as-is
811
- }
812
- const filteredPayload = { ...payload };
813
- // Get fields from the content type - handle both QikField and QikAIField structures
814
- let fields = [];
815
- if (typeInfo.fields) {
816
- fields = typeInfo.fields;
817
- }
818
- // Filter out readOnly fields from root level
819
- for (const field of fields) {
820
- if (field.readOnly && field.key && filteredPayload[field.key] !== undefined) {
821
- this.log(`Filtering out readOnly field: ${field.key}`);
822
- delete filteredPayload[field.key];
823
- }
824
- }
825
- // Filter out readOnly fields from data object
826
- if (filteredPayload.data && typeof filteredPayload.data === 'object') {
827
- for (const field of fields) {
828
- if (field.readOnly) {
829
- // Handle both QikAIField (with path) and QikField (with key) structures
830
- let dataFieldKey = null;
831
- if (field.path && field.path.startsWith('data.')) {
832
- // QikAIField structure
833
- dataFieldKey = field.path.replace('data.', '');
834
- }
835
- else if (field.key) {
836
- // QikField structure - assume it's a data field if it's readOnly and not at root
837
- dataFieldKey = field.key;
838
- }
839
- if (dataFieldKey && filteredPayload.data[dataFieldKey] !== undefined) {
840
- this.log(`Filtering out readOnly data field: ${dataFieldKey}`);
841
- delete filteredPayload.data[dataFieldKey];
842
- }
843
- }
844
- }
845
- }
846
- return filteredPayload;
847
- }
848
- generateEnhancedFilterSchema() {
849
- return {
850
- type: 'object',
851
- description: `Advanced filter criteria using Qik's powerful filter syntax. Supports hierarchical filters with 'and', 'or', 'nor' operators and 40+ comparators for dates, strings, numbers, and arrays.
852
-
853
- EXAMPLES:
854
-
855
- 1. Birthdays in next 10 days:
856
- {
857
- "operator": "and",
858
- "filters": [{
859
- "key": "dob",
860
- "comparator": "anniversarynext",
861
- "value": 10,
862
- "value2": "days"
863
- }]
864
- }
865
-
866
- 2. Male profiles born this month:
867
- {
868
- "operator": "and",
869
- "filters": [
870
- {"key": "gender", "comparator": "equal", "value": "male"},
871
- {"key": "dobMonth", "comparator": "equal", "value": 8}
872
- ]
873
- }
874
-
875
- 3. Content created in last 30 days:
876
- {
877
- "operator": "and",
878
- "filters": [{
879
- "key": "meta.created",
880
- "comparator": "datepast",
881
- "value": 30,
882
- "value2": "days"
883
- }]
884
- }
885
-
886
- 4. Complex query with OR logic:
887
- {
888
- "operator": "or",
889
- "filters": [
890
- {"key": "firstName", "comparator": "startswith", "value": "John"},
891
- {"key": "lastName", "comparator": "contains", "value": "Smith"}
892
- ]
893
- }`,
894
- properties: {
895
- operator: {
896
- type: 'string',
897
- enum: ['and', 'or', 'nor'],
898
- description: 'Logical operator: "and" (all must match), "or" (any can match), "nor" (none can match)'
899
- },
900
- filters: {
901
- type: 'array',
902
- description: 'Array of filter conditions or nested filter groups',
903
- items: {
904
- oneOf: [
905
- {
906
- type: 'object',
907
- description: 'Filter condition',
908
- properties: {
909
- key: {
910
- type: 'string',
911
- description: 'Field path to filter on (e.g., "firstName", "meta.created", "data.customField")'
912
- },
913
- comparator: {
914
- type: 'string',
915
- enum: [
916
- // Date/Anniversary comparators
917
- 'dateanniversary', 'anniversarybetween', 'anniversarynext', 'anniversarypast',
918
- 'datenext', 'datenotbetween', 'datenotnext', 'datebefore', 'dateafter',
919
- 'datetoday', 'datenottoday', 'datebeforetoday', 'dateaftertoday',
920
- 'datebeforenow', 'dateafternow', 'datepast', 'datenotpast',
921
- 'datesameday', 'datesamemonth', 'datesameweek', 'datesameyear',
922
- 'datebetween', 'datemonth',
923
- // String comparators
924
- 'equal', 'notequal', 'in', 'notin', 'startswith', 'doesnotstartwith',
925
- 'endswith', 'doesnotendwith', 'contains', 'excludes',
926
- // Numeric comparators
927
- 'greater', 'lesser', 'greaterequal', 'lesserequal',
928
- 'notgreater', 'notlesser', 'notgreaterequal', 'notlesserequal',
929
- 'between', 'notbetween',
930
- // Array/value comparators
931
- 'valuesgreater', 'valueslesser', 'valuesgreaterequal', 'valueslesserequal',
932
- 'empty', 'notempty'
933
- ],
934
- description: 'Comparison operator - see documentation for full list and usage'
935
- },
936
- value: {
937
- description: 'Primary comparison value (type depends on comparator)'
938
- },
939
- value2: {
940
- description: 'Secondary value for range comparators (between, anniversarynext, etc.)'
941
- },
942
- values: {
943
- type: 'array',
944
- description: 'Array of values for "in" and "notin" comparators'
945
- }
946
- },
947
- required: ['key', 'comparator']
948
- },
949
- {
950
- type: 'object',
951
- description: 'Nested filter group',
952
- properties: {
953
- operator: { type: 'string', enum: ['and', 'or', 'nor'] },
954
- filters: { type: 'array' }
955
- },
956
- required: ['operator', 'filters']
957
- }
958
- ]
959
- }
960
- }
961
- }
962
- };
963
- }
964
- /**
965
- * Enhanced intelligent content creation with advanced disambiguation logic
966
- *
967
- * This method provides sophisticated analysis of user intent to distinguish between:
968
- * - Creating workflow DEFINITIONS vs workflow CARD instances
969
- * - Creating content type DEFINITIONS vs content INSTANCES
970
- * - Understanding context clues like "add person to workflow" vs "create new workflow"
971
- */
972
- async intelligentContentCreation(description, additionalData) {
973
- // STEP 1: Advanced Intent Analysis with Disambiguation Logic
974
- const intentAnalysis = this.analyzeUserIntent(description, additionalData);
975
- // Handle workflow-specific disambiguation
976
- if (intentAnalysis.isWorkflowRelated) {
977
- return await this.handleWorkflowDisambiguation(description, additionalData, intentAnalysis);
978
- }
979
- // STEP 2: Standard content type matching
980
- const contentTypeMatches = this.findContentTypesByDescription(description);
981
- if (contentTypeMatches.length === 0) {
982
- return await this.handleNoContentTypeMatches(description);
983
- }
984
- if (contentTypeMatches.length > 1) {
985
- return await this.handleMultipleContentTypeMatches(description, contentTypeMatches);
986
- }
987
- // STEP 3: Single match found - provide comprehensive guidance
988
- const contentType = contentTypeMatches[0];
989
- return await this.handleSingleContentTypeMatch(contentType, description, additionalData);
990
- }
991
- /**
992
- * Analyzes user intent to distinguish between different types of content creation
993
- */
994
- analyzeUserIntent(description, additionalData) {
995
- const normalizedDesc = description.toLowerCase().trim();
996
- const contextClues = [];
997
- // Workflow-related keywords
998
- const workflowKeywords = ['workflow', 'kanban', 'board', 'column', 'step', 'process', 'pipeline'];
999
- const isWorkflowRelated = workflowKeywords.some(keyword => normalizedDesc.includes(keyword));
1000
- // Definition creation indicators
1001
- const definitionIndicators = [
1002
- 'create a new', 'create new', 'make a new', 'design a', 'set up a', 'build a',
1003
- 'define a', 'establish a', 'configure a'
1004
- ];
1005
- const isDefinitionCreation = definitionIndicators.some(indicator => normalizedDesc.includes(indicator));
1006
- // Instance creation indicators
1007
- const instanceIndicators = [
1008
- 'add', 'assign', 'put', 'move', 'place', 'insert', 'include'
1009
- ];
1010
- const isInstanceCreation = instanceIndicators.some(indicator => normalizedDesc.includes(indicator));
1011
- // Person assignment indicators
1012
- const personIndicators = [
1013
- 'add person', 'assign person', 'add user', 'assign user', 'add someone', 'assign someone',
1014
- 'add jim', 'add john', 'add sarah', 'put person', 'move person'
1015
- ];
1016
- const isPersonAssignment = personIndicators.some(indicator => normalizedDesc.includes(indicator));
1017
- // Collect context clues
1018
- if (isWorkflowRelated)
1019
- contextClues.push('workflow-related');
1020
- if (isDefinitionCreation)
1021
- contextClues.push('definition-creation');
1022
- if (isInstanceCreation)
1023
- contextClues.push('instance-creation');
1024
- if (isPersonAssignment)
1025
- contextClues.push('person-assignment');
1026
- // Calculate confidence based on clarity of intent
1027
- let confidence = 0.5; // Base confidence
1028
- if (isDefinitionCreation && !isInstanceCreation)
1029
- confidence = 0.9;
1030
- if (isInstanceCreation && !isDefinitionCreation)
1031
- confidence = 0.9;
1032
- if (isPersonAssignment)
1033
- confidence = 0.95;
1034
- return {
1035
- isWorkflowRelated,
1036
- isDefinitionCreation,
1037
- isInstanceCreation,
1038
- isPersonAssignment,
1039
- confidence,
1040
- contextClues
1041
- };
1042
- }
1043
- /**
1044
- * Handles workflow-specific disambiguation with comprehensive guidance
1045
- */
1046
- async handleWorkflowDisambiguation(description, additionalData, intentAnalysis) {
1047
- const normalizedDesc = description.toLowerCase().trim();
1048
- // Check if user wants to create a workflow DEFINITION
1049
- if (intentAnalysis.isDefinitionCreation ||
1050
- normalizedDesc.includes('create a workflow') ||
1051
- normalizedDesc.includes('new workflow') ||
1052
- normalizedDesc.includes('design workflow')) {
1053
- return {
1054
- content: [{
1055
- type: 'text',
1056
- text: `🔧 **WORKFLOW DEFINITION CREATION**
1057
-
1058
- You want to create a new workflow definition (template). This defines the structure, columns, steps, and automation rules.
1059
-
1060
- **WORKFLOW DEFINITION STRUCTURE:**
1061
-
1062
- A workflow definition includes:
1063
- - **Columns**: Stages like "To Do", "In Progress", "Review", "Done"
1064
- - **Steps**: Specific positions within columns where cards can be placed
1065
- - **Automation**: Functions that run when cards enter/exit steps
1066
- - **Due Date Behavior**: How due dates are calculated and managed
1067
- - **Completion Criteria**: Rules that determine when workflow is complete
1068
-
1069
- **EXAMPLE WORKFLOW DEFINITION:**
1070
- \`\`\`json
1071
- {
1072
- "type": "definition",
1073
- "title": "New Student Induction Workflow",
1074
- "definesType": "workflowcard",
1075
- "workflow": [
1076
- {
1077
- "title": "Enrollment",
1078
- "description": "Initial enrollment and documentation",
1079
- "steps": [
1080
- {
1081
- "title": "Application Received",
1082
- "type": "step",
1083
- "description": "Student application has been received",
1084
- "duration": 1440,
1085
- "assignees": [],
1086
- "entryFunction": "// Code to run when card enters this step",
1087
- "exitFunction": "// Code to run when card exits this step"
1088
- }
1089
- ]
1090
- },
1091
- {
1092
- "title": "Processing",
1093
- "description": "Review and approval process",
1094
- "steps": [
1095
- {
1096
- "title": "Document Review",
1097
- "type": "step",
1098
- "description": "Review all submitted documents"
1099
- }
1100
- ]
1101
- }
1102
- ]
1103
- }
1104
- \`\`\`
1105
-
1106
- To create this workflow definition, use:
1107
- \`qik_create_content\` with type: "definition" and the workflow structure in the data field.
1108
-
1109
- Would you like me to help you create a specific workflow definition?`,
1110
- }],
1111
- };
1112
- }
1113
- // Check if user wants to add someone to an existing workflow (create workflow CARD)
1114
- if (intentAnalysis.isPersonAssignment ||
1115
- normalizedDesc.includes('add') && normalizedDesc.includes('to workflow') ||
1116
- normalizedDesc.includes('assign') && normalizedDesc.includes('workflow')) {
1117
- return {
1118
- content: [{
1119
- type: 'text',
1120
- text: `👤 **WORKFLOW CARD CREATION (Person Assignment)**
1121
-
1122
- You want to add a person to an existing workflow by creating a workflow card instance.
1123
-
1124
- **WORKFLOW CARD vs WORKFLOW DEFINITION:**
1125
- - **Workflow Definition**: The template/structure (columns, steps, rules)
1126
- - **Workflow Card**: Individual items that move through the workflow
1127
-
1128
- **TO ADD SOMEONE TO A WORKFLOW:**
1129
-
1130
- 1. **Find the workflow definition ID** first using:
1131
- \`qik_list_content\` with type: "definition" and search for your workflow name
1132
-
1133
- 2. **Create a workflow card** using:
1134
- \`qik_create_content\` with type: "workflowcard"
1135
-
1136
- **EXAMPLE WORKFLOW CARD:**
1137
- \`\`\`json
1138
- {
1139
- "type": "workflowcard",
1140
- "title": "John Smith - Student Induction",
1141
- "reference": "PROFILE_ID_HERE",
1142
- "referenceType": "profile",
1143
- "data": {
1144
- "workflowDefinition": "WORKFLOW_DEFINITION_ID_HERE",
1145
- "currentStep": "application-received",
1146
- "assignedTo": ["USER_ID_HERE"],
1147
- "dueDate": "2024-01-15T09:00:00.000Z"
1148
- }
1149
- }
1150
- \`\`\`
1151
-
1152
- **NEED MORE HELP?**
1153
- - What's the name of the workflow you want to add someone to?
1154
- - Who do you want to add to the workflow?
1155
- - Do you have the workflow definition ID?`,
1156
- }],
1157
- };
1158
- }
1159
- // General workflow guidance
1160
- return {
1161
- content: [{
1162
- type: 'text',
1163
- text: `🔄 **WORKFLOW SYSTEM GUIDANCE**
1164
-
1165
- I detected you're working with workflows. Please clarify your intent:
1166
-
1167
- **OPTION 1: Create Workflow Definition (Template)**
1168
- - "Create a new workflow"
1169
- - "Design a student onboarding workflow"
1170
- - "Set up a project management workflow"
1171
- → Creates the structure, columns, steps, and rules
1172
-
1173
- **OPTION 2: Add Person to Existing Workflow**
1174
- - "Add Jim to the student workflow"
1175
- - "Assign Sarah to project workflow"
1176
- - "Put John in the onboarding process"
1177
- → Creates a workflow card instance for a person
1178
-
1179
- **WORKFLOW CONCEPTS:**
1180
- - **Definition**: The template (like a Kanban board layout)
1181
- - **Card**: Individual items moving through the workflow
1182
- - **Columns**: Stages (To Do, In Progress, Done)
1183
- - **Steps**: Specific positions within columns
1184
- - **Automation**: Code that runs when cards move
1185
-
1186
- **AVAILABLE WORKFLOW CONTENT TYPES:**
1187
- - \`definition\`: For creating workflow templates
1188
- - \`workflowcard\`: For individual workflow instances
1189
- - \`object\`: For custom workflow-related objects
1190
-
1191
- Please specify: Are you creating a new workflow template, or adding someone to an existing workflow?`,
1192
- }],
1193
- };
1194
- }
1195
- /**
1196
- * Handles cases where no content types match the description
1197
- */
1198
- async handleNoContentTypeMatches(description) {
1199
- const availableTypes = Object.entries(this.glossary).map(([key, type]) => {
1200
- let title = 'Unknown';
1201
- let plural = 'Unknown';
1202
- if (type && typeof type === 'object') {
1203
- if (type.definition) {
1204
- title = type.definition.title || title;
1205
- plural = type.definition.plural || plural;
1206
- }
1207
- else if (type.type) {
1208
- title = type.type.title || title;
1209
- plural = type.type.plural || plural;
1210
- }
1211
- else if (type.title) {
1212
- title = type.title || title;
1213
- plural = type.plural || plural;
1214
- }
1215
- }
1216
- return { key, title, plural };
1217
- });
1218
- // Group types by category for better organization
1219
- const categorizedTypes = this.categorizeContentTypes(availableTypes);
1220
- return {
1221
- content: [{
1222
- type: 'text',
1223
- text: `❌ **NO MATCHING CONTENT TYPE FOUND**
1224
-
1225
- I couldn't find a content type for "${description}".
1226
-
1227
- **AVAILABLE CONTENT TYPES BY CATEGORY:**
1228
-
1229
- ${categorizedTypes}
1230
-
1231
- **SUGGESTIONS:**
1232
- - Try using more specific terms (e.g., "incident report" instead of "report")
1233
- - Check if you meant to create a workflow definition or workflow card
1234
- - Use the exact content type key from the list above
1235
-
1236
- **NEED HELP?**
1237
- - Use \`qik_get_glossary\` to see all available types with descriptions
1238
- - Use \`qik_get_content_definition\` with a specific type to see its fields`,
1239
- }],
1240
- isError: true,
1241
- };
1242
- }
1243
- /**
1244
- * Categorizes content types for better organization in help text
1245
- */
1246
- categorizeContentTypes(types) {
1247
- const categories = {
1248
- 'Core Content': [],
1249
- 'People & Profiles': [],
1250
- 'Workflows & Processes': [],
1251
- 'Communication': [],
1252
- 'Media & Files': [],
1253
- 'System & Admin': [],
1254
- 'Other': []
1255
- };
1256
- for (const type of types) {
1257
- const key = type.key.toLowerCase();
1258
- const title = type.title.toLowerCase();
1259
- if (key.includes('profile') || key.includes('person') || key.includes('user')) {
1260
- categories['People & Profiles'].push(type);
1261
- }
1262
- else if (key.includes('workflow') || key.includes('definition') || key.includes('process')) {
1263
- categories['Workflows & Processes'].push(type);
1264
- }
1265
- else if (key.includes('comment') || key.includes('message') || key.includes('notification') || key.includes('email')) {
1266
- categories['Communication'].push(type);
1267
- }
1268
- else if (key.includes('file') || key.includes('image') || key.includes('video') || key.includes('audio')) {
1269
- categories['Media & Files'].push(type);
1270
- }
1271
- else if (key.includes('scope') || key.includes('role') || key.includes('policy') || key.includes('variable')) {
1272
- categories['System & Admin'].push(type);
1273
- }
1274
- else if (['article', 'event', 'object'].includes(key)) {
1275
- categories['Core Content'].push(type);
1276
- }
1277
- else {
1278
- categories['Other'].push(type);
1279
- }
1280
- }
1281
- let result = '';
1282
- for (const [category, categoryTypes] of Object.entries(categories)) {
1283
- if (categoryTypes.length > 0) {
1284
- result += `\n**${category}:**\n`;
1285
- result += categoryTypes.map(t => `- ${t.key}: ${t.title} (${t.plural})`).join('\n');
1286
- result += '\n';
1287
- }
1288
- }
1289
- return result;
1290
- }
1291
- /**
1292
- * Handles cases where multiple content types match
1293
- */
1294
- async handleMultipleContentTypeMatches(description, contentTypeMatches) {
1295
- const matchDetails = contentTypeMatches.map(key => {
1296
- const type = this.glossary[key];
1297
- let title = 'Unknown';
1298
- let plural = 'Unknown';
1299
- let baseType = '';
1300
- if (type && typeof type === 'object') {
1301
- if (type.definition) {
1302
- title = type.definition.title || title;
1303
- plural = type.definition.plural || plural;
1304
- baseType = type.definition.definesType || '';
1305
- }
1306
- else if (type.type) {
1307
- title = type.type.title || title;
1308
- plural = type.type.plural || plural;
1309
- }
1310
- else if (type.title) {
1311
- title = type.title || title;
1312
- plural = type.plural || plural;
1313
- baseType = type.definesType || '';
1314
- }
1315
- }
1316
- return { key, title, plural, baseType };
1317
- });
1318
- return {
1319
- content: [{
1320
- type: 'text',
1321
- text: `🔍 **MULTIPLE CONTENT TYPES FOUND**
1322
-
1323
- I found multiple content types that match "${description}". Please clarify which one you'd like to create:
1324
-
1325
- ${matchDetails.map(t => {
1326
- let description = `- **${t.key}**: ${t.title} (${t.plural})`;
1327
- if (t.baseType) {
1328
- description += ` - extends ${t.baseType}`;
1329
- }
1330
- return description;
1331
- }).join('\n')}
1332
-
1333
- **TO PROCEED:**
1334
- 1. Choose the exact content type key from above
1335
- 2. Use \`qik_create_content\` with your chosen type
1336
- 3. Or use \`qik_get_content_definition\` to see field details first
1337
-
1338
- **NEED MORE INFO?**
1339
- Use \`qik_get_content_definition\` with any of the type keys above to see their specific fields and requirements.`,
1340
- }],
1341
- isError: true,
1342
- };
1343
- }
1344
- /**
1345
- * Handles single content type match with comprehensive guidance
1346
- */
1347
- async handleSingleContentTypeMatch(contentType, description, additionalData) {
1348
- const typeInfo = this.getContentTypeInfo(contentType);
1349
- if (!typeInfo) {
1350
- return {
1351
- content: [{
1352
- type: 'text',
1353
- text: `❌ Found content type "${contentType}" but couldn't load its definition.`,
1354
- }],
1355
- isError: true,
1356
- };
1357
- }
1358
- // Extract comprehensive type information
1359
- const typeAnalysis = this.analyzeContentTypeStructure(typeInfo);
1360
- let guidance = `✅ **CONTENT TYPE FOUND: ${contentType.toUpperCase()}**\n\n`;
1361
- guidance += `**Type**: ${typeAnalysis.title}\n`;
1362
- guidance += `**Description**: ${typeAnalysis.description || 'No description available'}\n`;
1363
- if (typeAnalysis.baseType) {
1364
- guidance += `**Extends**: ${typeAnalysis.baseType}\n`;
1365
- }
1366
- guidance += `\n**FIELD STRUCTURE:**\n`;
1367
- if (typeAnalysis.requiredFields.length > 0) {
1368
- guidance += `\n**Required Fields:**\n`;
1369
- guidance += typeAnalysis.requiredFields.map(f => `- **${f.key}** (${f.title}): ${f.description || 'No description'}`).join('\n');
1370
- }
1371
- if (typeAnalysis.optionalFields.length > 0) {
1372
- guidance += `\n\n**Optional Fields:**\n`;
1373
- guidance += typeAnalysis.optionalFields.map(f => `- **${f.key}** (${f.title}): ${f.description || 'No description'}`).join('\n');
1374
- }
1375
- // Handle creation if data provided
1376
- if (additionalData && typeof additionalData === 'object' && additionalData.title) {
1377
- return await this.handleContentCreationWithData(contentType, additionalData, typeAnalysis);
1378
- }
1379
- // Provide creation guidance
1380
- guidance += await this.generateCreationGuidance(contentType, typeAnalysis);
1381
- return {
1382
- content: [{
1383
- type: 'text',
1384
- text: guidance,
1385
- }],
1386
- };
1387
- }
1388
- /**
1389
- * Analyzes content type structure for comprehensive information
1390
- */
1391
- analyzeContentTypeStructure(typeInfo) {
1392
- let fields = [];
1393
- let title = 'Unknown';
1394
- let description = '';
1395
- let baseType = '';
1396
- if (typeInfo.definition) {
1397
- fields = typeInfo.definition.fields || [];
1398
- title = typeInfo.definition.title || title;
1399
- description = typeInfo.definition.description || '';
1400
- baseType = typeInfo.definition.definesType || '';
1401
- }
1402
- else if (typeInfo.type) {
1403
- fields = typeInfo.type.fields || [];
1404
- title = typeInfo.type.title || title;
1405
- description = typeInfo.type.description || '';
1406
- }
1407
- else if (typeInfo.fields) {
1408
- fields = typeInfo.fields || [];
1409
- title = typeInfo.title || title;
1410
- description = typeInfo.description || '';
1411
- baseType = typeInfo.definesType || '';
1412
- }
1413
- const requiredFields = fields.filter((f) => f.minimum && f.minimum > 0);
1414
- const optionalFields = fields.filter((f) => !f.minimum || f.minimum === 0);
1415
- return {
1416
- title,
1417
- description,
1418
- baseType,
1419
- fields,
1420
- requiredFields,
1421
- optionalFields,
1422
- fieldCount: fields.length
1423
- };
1424
- }
1425
- /**
1426
- * Handles content creation when data is provided
1427
- */
1428
- async handleContentCreationWithData(contentType, additionalData, typeAnalysis) {
1429
- // Check if scopes are provided
1430
- if (!additionalData.meta || !additionalData.meta.scopes || !Array.isArray(additionalData.meta.scopes) || additionalData.meta.scopes.length === 0) {
1431
- const scopeTree = await this.getAvailableScopes();
1432
- if (scopeTree) {
1433
- const availableScopes = this.extractScopesWithPermissions(scopeTree, 'create');
1434
- if (availableScopes.length === 0) {
1435
- return {
1436
- content: [{
1437
- type: 'text',
1438
- text: `🚫 **PERMISSION DENIED**\n\nYou don't have permission to create content in any scopes. Please contact your administrator.`,
1439
- }],
1440
- isError: true,
1441
- };
1442
- }
1443
- return {
1444
- content: [{
1445
- type: 'text',
1446
- text: `📍 **SCOPE SELECTION REQUIRED**\n\nTo create "${additionalData.title}" as a ${typeAnalysis.title}, you need to specify which scope to create it in.\n\n**Available Scopes:**\n${availableScopes.map(s => `- **${s.id}**: ${s.path}`).join('\n')}\n\n**TO CREATE:**\nUse \`qik_create_content\` with:\n- type: "${contentType}"\n- title: "${additionalData.title}"\n- meta: { "scopes": ["scope_id_here"] }\n- data: { /* your field values */ }`,
1447
- }],
1448
- };
1449
- }
1450
- }
1451
- // Proceed with creation
1452
- const title = additionalData.title || `New ${typeAnalysis.title}`;
1453
- const data = additionalData.data || {};
1454
- const meta = additionalData.meta || {};
1455
- return await this.createContent({
1456
- type: contentType,
1457
- title,
1458
- data,
1459
- meta,
1460
- });
1461
- }
1462
- /**
1463
- * Generates comprehensive creation guidance
1464
- */
1465
- async generateCreationGuidance(contentType, typeAnalysis) {
1466
- let guidance = `\n\n**CREATION GUIDANCE:**\n`;
1467
- // Get available scopes
1468
- const scopeTree = await this.getAvailableScopes();
1469
- let scopeGuidance = '';
1470
- if (scopeTree) {
1471
- const availableScopes = this.extractScopesWithPermissions(scopeTree, 'create');
1472
- if (availableScopes.length > 0) {
1473
- scopeGuidance = `\n**Available Scopes:**\n${availableScopes.map(s => `- **${s.id}**: ${s.path}`).join('\n')}\n`;
1474
- }
1475
- }
1476
- guidance += `\n**TO CREATE THIS CONTENT:**\n`;
1477
- guidance += `Use \`qik_create_content\` with:\n`;
1478
- guidance += `- **type**: "${contentType}"\n`;
1479
- guidance += `- **title**: "Your title here"\n`;
1480
- guidance += `- **meta**: { "scopes": ["scope_id_here"] } *(required)*\n`;
1481
- guidance += `- **data**: { /* field values based on structure above */ }\n`;
1482
- guidance += scopeGuidance;
1483
- // Add specific guidance for common types
1484
- if (contentType === 'definition') {
1485
- guidance += `\n**WORKFLOW DEFINITION EXAMPLE:**\n`;
1486
- guidance += `For workflow definitions, include the workflow structure in the data field with columns, steps, and automation rules.\n`;
1487
- }
1488
- if (contentType.includes('comment') || contentType.includes('Comment')) {
1489
- guidance += `\n**COMMENT CREATION:**\n`;
1490
- guidance += `Comments require a reference to the item being commented on. Include:\n`;
1491
- guidance += `- **reference**: ID of the item to comment on\n`;
1492
- guidance += `- **referenceType**: Type of the referenced item\n`;
1493
- }
1494
- return guidance;
1495
- }
1496
- generateToolDescription(baseDescription, emoji = '') {
1497
- const prefix = emoji ? `${emoji} ` : '';
1498
- return `${prefix}${baseDescription.replace(/Qik/g, this.serverName)}`;
1499
- }
1500
31
  setupToolHandlers() {
32
+ // List available tools
1501
33
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
1502
- // Ensure glossary is loaded
1503
- await this.loadGlossary();
1504
- const tools = [
1505
- // Authentication & Session
1506
- {
1507
- name: 'qik_get_user_session',
1508
- description: this.generateToolDescription('Get current user session information', '👤'),
1509
- inputSchema: {
1510
- type: 'object',
1511
- properties: {},
1512
- },
1513
- },
1514
- // Content Type Discovery
1515
- {
1516
- name: 'qik_get_glossary',
1517
- description: this.generateToolDescription('Get all available content types and their definitions', '📚'),
1518
- inputSchema: {
1519
- type: 'object',
1520
- properties: {},
1521
- },
1522
- },
1523
- {
1524
- name: 'qik_get_content_definition',
1525
- description: this.generateToolDescription('Get definition for a specific content type', '🔍'),
1526
- inputSchema: {
1527
- type: 'object',
1528
- properties: {
1529
- type: {
1530
- type: 'string',
1531
- description: 'Content type key (e.g., "article", "profile", "event")',
1532
- enum: Object.keys(this.glossary),
1533
- },
1534
- },
1535
- required: ['type'],
1536
- },
1537
- },
1538
- // Content Management
1539
- {
1540
- name: 'qik_get_content',
1541
- description: this.generateToolDescription('Get content item by ID or slug', '📄'),
1542
- inputSchema: {
1543
- type: 'object',
1544
- properties: {
1545
- id: {
1546
- type: 'string',
1547
- description: 'Content ID',
1548
- },
1549
- slug: {
1550
- type: 'string',
1551
- description: 'Content slug (e.g., "article:my-post" or "car:pathfinder")',
1552
- },
1553
- },
1554
- oneOf: [
1555
- { required: ['id'] },
1556
- { required: ['slug'] },
1557
- ],
1558
- },
1559
- },
1560
- {
1561
- name: 'qik_list_content',
1562
- description: this.generateToolDescription(`List content items with advanced filtering and search capabilities. Supports complex queries like birthdays, date ranges, and sophisticated business logic.
1563
-
1564
- **ENHANCED FILTER CAPABILITIES:**
1565
- - 40+ comparators for dates, strings, numbers, and arrays
1566
- - Hierarchical filters with 'and', 'or', 'nor' operators
1567
- - Anniversary and birthday queries (anniversarynext, anniversarypast)
1568
- - Date range filtering (datebetween, datepast, datenext)
1569
- - String matching (contains, startswith, endswith, equal)
1570
- - Numeric comparisons (greater, lesser, between)
1571
- - Array operations (in, notin, valuesgreater)
1572
-
1573
- **COMMON USE CASES:**
1574
- - Find birthdays in next 10 days: {"operator":"and","filters":[{"key":"dob","comparator":"anniversarynext","value":10,"value2":"days"}]}
1575
- - Recent content: {"operator":"and","filters":[{"key":"meta.created","comparator":"datepast","value":30,"value2":"days"}]}
1576
- - Gender filtering: {"operator":"and","filters":[{"key":"gender","comparator":"equal","value":"male"}]}
1577
- - Complex queries with OR logic for multiple conditions
1578
-
1579
- **FIELD TARGETING:**
1580
- - Use dot notation for nested fields: "meta.created", "data.customField"
1581
- - Target specific profile fields: "firstName", "lastName", "emails"
1582
- - Filter by metadata: "meta.scopes", "meta.tags", "meta.security"`, '📋'),
1583
- inputSchema: {
1584
- type: 'object',
1585
- properties: {
1586
- type: {
1587
- type: 'string',
1588
- description: 'Content type to list',
1589
- enum: Object.keys(this.glossary),
1590
- },
1591
- search: {
1592
- type: 'string',
1593
- description: 'Search keywords - searches within title, tags, and text areas',
1594
- },
1595
- filter: this.generateEnhancedFilterSchema(),
1596
- sort: {
1597
- type: 'object',
1598
- description: 'Sorting configuration for results',
1599
- properties: {
1600
- key: {
1601
- type: 'string',
1602
- description: 'Field to sort by (e.g., "title", "meta.created", "data.customField")'
1603
- },
1604
- direction: {
1605
- type: 'string',
1606
- enum: ['asc', 'desc'],
1607
- description: 'Sort direction: ascending or descending'
1608
- },
1609
- type: {
1610
- type: 'string',
1611
- enum: ['string', 'number', 'date'],
1612
- description: 'Data type for proper sorting behavior'
1613
- },
1614
- },
1615
- },
1616
- page: {
1617
- type: 'object',
1618
- description: 'Pagination settings',
1619
- properties: {
1620
- size: {
1621
- type: 'number',
1622
- minimum: 1,
1623
- maximum: 100,
1624
- description: 'Number of items per page (1-100)'
1625
- },
1626
- index: {
1627
- type: 'number',
1628
- minimum: 1,
1629
- description: 'Page number to retrieve (starts at 1)'
1630
- },
1631
- },
1632
- },
1633
- select: {
1634
- type: 'array',
1635
- items: { type: 'string' },
1636
- description: 'Specific fields to include in response (e.g., ["title", "data.make", "meta.created"])',
1637
- },
1638
- },
1639
- required: ['type'],
1640
- },
1641
- },
1642
- {
1643
- name: 'qik_create_content',
1644
- description: this.generateToolDescription(`Create new content item with intelligent field structure handling.
1645
-
1646
- **FIELD STRUCTURE INTELLIGENCE:**
1647
- - Automatically separates root-level fields from data object fields
1648
- - Handles comment inheritance (comments inherit scopes from referenced items)
1649
- - Validates field requirements based on content type definitions
1650
- - Supports workflow definitions, workflow cards, and all content types
1651
-
1652
- **FIELD PLACEMENT RULES:**
1653
- - **Root Level**: reference, referenceType, body, organisation, title, meta
1654
- - **Data Object**: Custom fields defined in content type definitions (definedFields)
1655
- - **Meta Object**: scopes (required), tags, security, personaAuthor, etc.
1656
-
1657
- **CONTENT TYPE EXAMPLES:**
1658
- - **Comments**: Require reference + referenceType, inherit scopes automatically
1659
- - **Workflow Definitions**: Use data object for workflow structure (columns, steps, automation)
1660
- - **Workflow Cards**: Reference profiles, link to workflow definitions
1661
- - **Profiles**: firstName, lastName at root, custom fields in data object
1662
- - **Articles**: body at root level, custom article fields in data object
1663
-
1664
- **SCOPE INHERITANCE:**
1665
- - Comments automatically inherit scopes from referenced items
1666
- - Other content types require explicit scope assignment
1667
- - Use qik_get_scopes to find available scopes with permissions
1668
-
1669
- **VALIDATION:**
1670
- - Checks content type exists and user has access
1671
- - Validates required fields based on content type definition
1672
- - Ensures proper field placement (root vs data object)`, '✨'),
1673
- inputSchema: {
1674
- type: 'object',
1675
- properties: {
1676
- type: {
1677
- type: 'string',
1678
- description: 'Content type to create (use qik_get_glossary to see all available types)',
1679
- enum: Object.keys(this.glossary),
1680
- },
1681
- title: {
1682
- type: 'string',
1683
- description: 'Content title (required for all content types)',
1684
- },
1685
- // Generate dynamic properties based on content types
1686
- ...this.generateDynamicContentProperties(),
1687
- meta: {
1688
- type: 'object',
1689
- description: 'Meta information (scopes required for most content types)',
1690
- properties: {
1691
- scopes: {
1692
- type: 'array',
1693
- items: { type: 'string' },
1694
- description: 'Scope IDs where this content should be stored (REQUIRED - use qik_get_scopes to find available)',
1695
- },
1696
- tags: {
1697
- type: 'array',
1698
- items: { type: 'string' },
1699
- description: 'Tag IDs for categorization and search',
1700
- },
1701
- security: {
1702
- type: 'string',
1703
- enum: ['public', 'secure', 'private'],
1704
- description: 'Security level: public (everyone), secure (authenticated), private (restricted)',
1705
- },
1706
- },
1707
- },
1708
- },
1709
- required: ['type', 'title'],
1710
- },
1711
- },
1712
- {
1713
- name: 'qik_update_content',
1714
- description: this.generateToolDescription('Update existing content item', '✏️'),
1715
- inputSchema: {
1716
- type: 'object',
1717
- properties: {
1718
- id: {
1719
- type: 'string',
1720
- description: 'Content ID to update',
1721
- },
1722
- data: {
1723
- type: 'object',
1724
- description: 'Data to update (partial update)',
1725
- },
1726
- replace: {
1727
- type: 'boolean',
1728
- description: 'Whether to replace entire content (PUT) or merge (PATCH)',
1729
- default: false,
1730
- },
1731
- },
1732
- required: ['id', 'data'],
1733
- },
1734
- },
1735
- {
1736
- name: 'qik_delete_content',
1737
- description: this.generateToolDescription('Delete content item', '🗑️'),
1738
- inputSchema: {
1739
- type: 'object',
1740
- properties: {
1741
- id: {
1742
- type: 'string',
1743
- description: 'Content ID to delete',
1744
- },
1745
- },
1746
- required: ['id'],
1747
- },
1748
- },
1749
- // Profile Management
1750
- {
1751
- name: 'qik_list_profiles',
1752
- description: this.generateToolDescription('Search and list profiles/people', '👥'),
1753
- inputSchema: {
1754
- type: 'object',
1755
- properties: {
1756
- search: {
1757
- type: 'string',
1758
- description: 'Search by name or email',
1759
- },
1760
- filter: {
1761
- type: 'object',
1762
- description: 'Filter criteria',
1763
- },
1764
- page: {
1765
- type: 'object',
1766
- properties: {
1767
- size: { type: 'number', minimum: 1, maximum: 100 },
1768
- index: { type: 'number', minimum: 1 },
1769
- },
1770
- },
1771
- },
1772
- },
1773
- },
1774
- {
1775
- name: 'qik_create_profile',
1776
- description: this.generateToolDescription('Create new profile/person', '👤'),
1777
- inputSchema: {
1778
- type: 'object',
1779
- properties: {
1780
- firstName: {
1781
- type: 'string',
1782
- description: 'First name',
1783
- },
1784
- lastName: {
1785
- type: 'string',
1786
- description: 'Last name',
1787
- },
1788
- emails: {
1789
- type: 'array',
1790
- items: { type: 'string' },
1791
- description: 'Email addresses',
1792
- },
1793
- phoneNumbers: {
1794
- type: 'array',
1795
- items: {
1796
- type: 'object',
1797
- properties: {
1798
- label: { type: 'string' },
1799
- countryCode: { type: 'string' },
1800
- number: { type: 'string' },
1801
- },
1802
- },
1803
- description: 'Phone numbers',
1804
- },
1805
- data: {
1806
- type: 'object',
1807
- description: 'Additional profile data',
1808
- },
1809
- meta: {
1810
- type: 'object',
1811
- description: 'Meta information (scopes, tags, etc.)',
1812
- },
1813
- },
1814
- required: ['firstName', 'lastName'],
1815
- },
1816
- },
1817
- {
1818
- name: 'qik_get_profile_timeline',
1819
- description: this.generateToolDescription('Get a profile timeline with chronological activity data - provides much richer information than basic profile details including recent actions, content created, events attended, workflow activities, and other activity logs', '📅'),
1820
- inputSchema: {
1821
- type: 'object',
1822
- properties: {
1823
- id: {
1824
- type: 'string',
1825
- description: 'Profile ID to get timeline for',
1826
- },
1827
- },
1828
- required: ['id'],
1829
- },
1830
- },
1831
- {
1832
- name: 'qik_get_profile_info',
1833
- description: this.generateToolDescription('Intelligently get profile information with disambiguation between basic details and activity timeline. Asks clarifying questions when the request is ambiguous to provide the most relevant information', '🤔'),
1834
- inputSchema: {
1835
- type: 'object',
1836
- properties: {
1837
- query: {
1838
- type: 'string',
1839
- description: 'Natural language query about a person (e.g., "Tell me about Jeff", "What has John been up to?", "Get Sarah\'s contact info")',
1840
- },
1841
- profileId: {
1842
- type: 'string',
1843
- description: 'Profile ID if known (optional - can search by name if not provided)',
1844
- },
1845
- profileName: {
1846
- type: 'string',
1847
- description: 'Person\'s name to search for if ID not provided (optional)',
1848
- },
1849
- includeTimeline: {
1850
- type: 'boolean',
1851
- description: 'Whether to include timeline/activity data (optional - will be determined from query if not specified)',
1852
- },
1853
- },
1854
- required: ['query'],
1855
- },
1856
- },
1857
- // Form Management
1858
- {
1859
- name: 'qik_get_form',
1860
- description: this.generateToolDescription('Get form definition', '📝'),
1861
- inputSchema: {
1862
- type: 'object',
1863
- properties: {
1864
- id: {
1865
- type: 'string',
1866
- description: 'Form ID',
1867
- },
1868
- },
1869
- required: ['id'],
1870
- },
1871
- },
1872
- {
1873
- name: 'qik_submit_form',
1874
- description: this.generateToolDescription('Submit form data', '📤'),
1875
- inputSchema: {
1876
- type: 'object',
1877
- properties: {
1878
- id: {
1879
- type: 'string',
1880
- description: 'Form ID',
1881
- },
1882
- data: {
1883
- type: 'object',
1884
- description: 'Form submission data',
1885
- },
1886
- },
1887
- required: ['id', 'data'],
1888
- },
1889
- },
1890
- // File Management
1891
- {
1892
- name: 'qik_upload_file',
1893
- description: this.generateToolDescription('Upload file to platform', '📁'),
1894
- inputSchema: {
1895
- type: 'object',
1896
- properties: {
1897
- title: {
1898
- type: 'string',
1899
- description: 'File title',
1900
- },
1901
- fileData: {
1902
- type: 'string',
1903
- description: 'Base64 encoded file data',
1904
- },
1905
- fileName: {
1906
- type: 'string',
1907
- description: 'Original file name',
1908
- },
1909
- mimeType: {
1910
- type: 'string',
1911
- description: 'File MIME type',
1912
- },
1913
- meta: {
1914
- type: 'object',
1915
- description: 'Meta information (scopes, tags, etc.)',
1916
- },
1917
- },
1918
- required: ['title', 'fileData', 'fileName'],
1919
- },
1920
- },
1921
- // Search & Discovery
1922
- {
1923
- name: 'qik_search_content',
1924
- description: this.generateToolDescription('Global content search across all types', '🔎'),
1925
- inputSchema: {
1926
- type: 'object',
1927
- properties: {
1928
- query: {
1929
- type: 'string',
1930
- description: 'Search query',
1931
- },
1932
- types: {
1933
- type: 'array',
1934
- items: {
1935
- type: 'string',
1936
- enum: Object.keys(this.glossary),
1937
- },
1938
- description: 'Content types to search in',
1939
- },
1940
- limit: {
1941
- type: 'number',
1942
- minimum: 1,
1943
- maximum: 100,
1944
- default: 20,
1945
- },
1946
- },
1947
- required: ['query'],
1948
- },
1949
- },
1950
- {
1951
- name: 'qik_get_scopes',
1952
- description: this.generateToolDescription('Get available scopes/permissions tree', '🔐'),
1953
- inputSchema: {
1954
- type: 'object',
1955
- properties: {},
1956
- },
1957
- },
1958
- // Utility Tools
1959
- {
1960
- name: 'qik_get_smartlist',
1961
- description: this.generateToolDescription('Execute a smartlist query', '📊'),
1962
- inputSchema: {
1963
- type: 'object',
1964
- properties: {
1965
- id: {
1966
- type: 'string',
1967
- description: 'Smartlist ID',
1968
- },
1969
- },
1970
- required: ['id'],
1971
- },
1972
- },
1973
- // Intelligent Content Creation with Advanced Disambiguation
1974
- {
1975
- name: 'qik_create_content_intelligent',
1976
- description: this.generateToolDescription(`Intelligently create content with advanced disambiguation logic.
1977
-
1978
- **WORKFLOW DISAMBIGUATION:**
1979
- - "create a workflow" → Creates workflow DEFINITION (template)
1980
- - "add Jim to workflow X" → Creates workflow CARD (instance)
1981
- - "design student onboarding workflow" → Creates workflow DEFINITION
1982
- - "assign Sarah to project workflow" → Creates workflow CARD
1983
-
1984
- **CONTENT TYPE DISAMBIGUATION:**
1985
- - Automatically detects intent between definitions vs instances
1986
- - Provides comprehensive guidance for workflow systems
1987
- - Categorizes content types for better organization
1988
- - Handles scope permissions and requirements
1989
-
1990
- **EXAMPLES:**
1991
- - "create an incident report" → Finds incident report content type
1992
- - "make a new workflow" → Guides through workflow definition creation
1993
- - "add person to existing workflow" → Guides through workflow card creation
1994
- - "design a student induction process" → Creates workflow definition
1995
-
1996
- **WORKFLOW CONCEPTS EXPLAINED:**
1997
- - **Workflow Definition**: Template with columns, steps, automation rules
1998
- - **Workflow Card**: Individual items that move through the workflow
1999
- - **Columns**: Stages like "To Do", "In Progress", "Done"
2000
- - **Steps**: Specific positions within columns
2001
- - **Automation**: Entry/exit/success/fail functions`, '🧠'),
2002
- inputSchema: {
2003
- type: 'object',
2004
- properties: {
2005
- description: {
2006
- type: 'string',
2007
- description: 'Natural language description of what to create. Examples: "create an incident report", "make a new workflow", "add Jim to student workflow", "design onboarding process"',
2008
- },
2009
- title: {
2010
- type: 'string',
2011
- description: 'Title for the content (optional - will be requested if needed)',
2012
- },
2013
- data: {
2014
- type: 'object',
2015
- description: 'Content data fields (optional - will be guided through structure)',
2016
- },
2017
- meta: {
2018
- type: 'object',
2019
- description: 'Meta information including scopes, tags, security level (will be guided through requirements)',
2020
- properties: {
2021
- scopes: {
2022
- type: 'array',
2023
- items: { type: 'string' },
2024
- description: 'Scope IDs where content should be created'
2025
- },
2026
- tags: {
2027
- type: 'array',
2028
- items: { type: 'string' },
2029
- description: 'Tag IDs for categorization'
2030
- },
2031
- security: {
2032
- type: 'string',
2033
- enum: ['public', 'secure', 'private'],
2034
- description: 'Security level for the content'
2035
- }
2036
- }
2037
- },
2038
- },
2039
- required: ['description'],
2040
- },
2041
- },
2042
- ];
2043
34
  return { tools };
2044
35
  });
36
+ // Handle tool calls
2045
37
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
2046
- try {
2047
- // Ensure glossary is fresh
2048
- await this.loadGlossary();
2049
- switch (request.params.name) {
2050
- case 'qik_get_user_session':
2051
- return await this.getUserSession();
2052
- case 'qik_get_glossary':
2053
- return await this.getGlossary();
2054
- case 'qik_get_content_definition':
2055
- return await this.getContentDefinition(request.params.arguments);
2056
- case 'qik_get_content':
2057
- return await this.getContent(request.params.arguments);
2058
- case 'qik_list_content':
2059
- return await this.listContent(request.params.arguments);
2060
- case 'qik_create_content':
2061
- return await this.createContent(request.params.arguments);
2062
- case 'qik_update_content':
2063
- return await this.updateContent(request.params.arguments);
2064
- case 'qik_delete_content':
2065
- return await this.deleteContent(request.params.arguments);
2066
- case 'qik_list_profiles':
2067
- return await this.listProfiles(request.params.arguments);
2068
- case 'qik_create_profile':
2069
- return await this.createProfile(request.params.arguments);
2070
- case 'qik_get_form':
2071
- return await this.getForm(request.params.arguments);
2072
- case 'qik_submit_form':
2073
- return await this.submitForm(request.params.arguments);
2074
- case 'qik_upload_file':
2075
- return await this.uploadFile(request.params.arguments);
2076
- case 'qik_search_content':
2077
- return await this.searchContent(request.params.arguments);
2078
- case 'qik_get_scopes':
2079
- return await this.getScopes();
2080
- case 'qik_get_smartlist':
2081
- return await this.getSmartlist(request.params.arguments);
2082
- case 'qik_get_profile_timeline':
2083
- return await this.getProfileTimeline(request.params.arguments);
2084
- case 'qik_get_profile_info':
2085
- return await this.getProfileInfo(request.params.arguments);
2086
- case 'qik_create_content_intelligent':
2087
- return await this.intelligentContentCreation(request.params.arguments.description, {
2088
- title: request.params.arguments.title,
2089
- data: request.params.arguments.data,
2090
- meta: request.params.arguments.meta,
2091
- });
2092
- default:
2093
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
2094
- }
2095
- }
2096
- catch (error) {
2097
- this.log(`Error in ${request.params.name}: ${this.formatError(error)}`);
2098
- if (axios.isAxiosError(error)) {
2099
- const axiosError = error;
2100
- if (axiosError.response?.status === 401) {
2101
- return {
2102
- content: [{
2103
- type: 'text',
2104
- text: 'Authentication failed. Please check your access token configuration.',
2105
- }],
2106
- isError: true,
2107
- };
2108
- }
2109
- else if (axiosError.response?.status === 403) {
2110
- return {
2111
- content: [{
2112
- type: 'text',
2113
- text: 'Access denied. Your token may not have permission for this operation.',
2114
- }],
2115
- isError: true,
2116
- };
2117
- }
2118
- else if (axiosError.response?.status === 429) {
2119
- return {
2120
- content: [{
2121
- type: 'text',
2122
- text: 'Rate limit exceeded. Please try again later.',
2123
- }],
2124
- isError: true,
2125
- };
2126
- }
2127
- }
2128
- return {
2129
- content: [{
2130
- type: 'text',
2131
- text: `Error: ${this.formatError(error)}`,
2132
- }],
2133
- isError: true,
2134
- };
2135
- }
2136
- });
2137
- }
2138
- // Enhanced tool implementations
2139
- async getUserSession() {
2140
- if (!this.userSession) {
2141
- await this.loadUserSession();
2142
- }
2143
- return {
2144
- content: [{
2145
- type: 'text',
2146
- text: JSON.stringify(this.userSession, null, 2),
2147
- }],
2148
- };
2149
- }
2150
- async getGlossary() {
2151
- await this.loadGlossary(true); // Force refresh
2152
- // Create a clean list of all content types with their proper titles and descriptions
2153
- const contentTypes = Object.entries(this.glossary).map(([key, contentType]) => {
2154
- let title = 'Unknown';
2155
- let plural = 'Unknown';
2156
- let description = '';
2157
- let fieldCount = 0;
2158
- let baseType = null;
2159
- let fields = [];
2160
- if (contentType && typeof contentType === 'object') {
2161
- // Handle direct properties on contentType (most common structure)
2162
- if (contentType.title) {
2163
- title = contentType.title;
2164
- plural = contentType.plural || title + 's';
2165
- description = contentType.description || '';
2166
- fields = contentType.fields || [];
2167
- baseType = contentType.definesType;
2168
- }
2169
- // Handle nested definition structure
2170
- else if (contentType.definition) {
2171
- title = contentType.definition.title || title;
2172
- plural = contentType.definition.plural || title + 's';
2173
- description = contentType.definition.description || '';
2174
- fields = contentType.definition.fields || [];
2175
- baseType = contentType.definition.definesType;
2176
- }
2177
- // Handle nested type structure
2178
- else if (contentType.type) {
2179
- title = contentType.type.title || title;
2180
- plural = contentType.type.plural || title + 's';
2181
- description = contentType.type.description || '';
2182
- fields = contentType.type.fields || [];
2183
- }
2184
- fieldCount = fields.length;
2185
- }
2186
- return {
2187
- key,
2188
- title,
2189
- plural,
2190
- description,
2191
- fieldCount,
2192
- baseType,
2193
- isExtension: baseType && baseType !== key,
2194
- fields: fields.map(f => ({
2195
- key: f.key,
2196
- title: f.title,
2197
- type: f.type,
2198
- required: this.isFieldRequired(f),
2199
- description: f.description || ''
2200
- }))
2201
- };
2202
- });
2203
- // Sort alphabetically by title for easy reading
2204
- const sortedTypes = contentTypes.sort((a, b) => a.title.localeCompare(b.title));
2205
- // Create a summary that clearly shows each type is unique
2206
- const typesList = sortedTypes.map(type => {
2207
- let typeDescription = `${type.key}: ${type.title}`;
2208
- if (type.isExtension && type.baseType) {
2209
- typeDescription += ` (extends ${type.baseType})`;
38
+ const { name, arguments: args } = request.params;
39
+ if (!hasToolHandler(name)) {
40
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
2210
41
  }
2211
- if (type.plural !== type.title + 's') {
2212
- typeDescription += ` - plural: ${type.plural}`;
2213
- }
2214
- if (type.fieldCount > 0) {
2215
- typeDescription += ` - ${type.fieldCount} fields`;
2216
- }
2217
- return typeDescription;
2218
- });
2219
- return {
2220
- content: [{
2221
- type: 'text',
2222
- text: `Available Content Types (${contentTypes.length} total):
2223
-
2224
- ${typesList.join('\n')}
2225
-
2226
- Each content type above is unique and can be used for content creation. Extended types (those that show "extends X") are specialized versions of base types with their own specific fields and purposes.
2227
-
2228
- For detailed field information about any content type, use qik_get_content_definition with the type key.
2229
-
2230
- Full glossary data:
2231
- ${JSON.stringify({
2232
- contentTypes: sortedTypes,
2233
- totalTypes: contentTypes.length,
2234
- extensionTypes: contentTypes.filter(t => t.isExtension).length,
2235
- baseTypes: contentTypes.filter(t => !t.isExtension).length
2236
- }, null, 2)}`,
2237
- }],
2238
- };
2239
- }
2240
- async getContentDefinition(args) {
2241
- const validation = this.validateContentType(args.type);
2242
- if (!validation.valid) {
2243
- return {
2244
- content: [{
2245
- type: 'text',
2246
- text: validation.error,
2247
- }],
2248
- isError: true,
2249
- };
2250
- }
2251
- try {
2252
- const response = await this.axiosInstance.get(`/content/${args.type}/definition`);
2253
- return {
2254
- content: [{
2255
- type: 'text',
2256
- text: JSON.stringify(response.data, null, 2),
2257
- }],
2258
- };
2259
- }
2260
- catch (error) {
2261
- return {
2262
- content: [{
2263
- type: 'text',
2264
- text: `Failed to fetch definition for '${args.type}': ${this.formatError(error)}`,
2265
- }],
2266
- isError: true,
2267
- };
2268
- }
2269
- }
2270
- async getContent(args) {
2271
- let response;
2272
- try {
2273
- if (args.id) {
2274
- response = await this.axiosInstance.get(`/content/${args.id}`);
2275
- }
2276
- else if (args.slug) {
2277
- response = await this.axiosInstance.get(`/content/slug/${args.slug}`);
2278
- }
2279
- else {
2280
- throw new Error('Either id or slug must be provided');
2281
- }
2282
- return {
2283
- content: [{
2284
- type: 'text',
2285
- text: JSON.stringify(response.data, null, 2),
2286
- }],
2287
- };
2288
- }
2289
- catch (error) {
2290
- const identifier = args.id || args.slug;
2291
- return {
2292
- content: [{
2293
- type: 'text',
2294
- text: `Failed to fetch content '${identifier}': ${this.formatError(error)}`,
2295
- }],
2296
- isError: true,
2297
- };
2298
- }
2299
- }
2300
- async listContent(args) {
2301
- const validation = this.validateContentType(args.type);
2302
- if (!validation.valid) {
2303
- return {
2304
- content: [{
2305
- type: 'text',
2306
- text: validation.error,
2307
- }],
2308
- isError: true,
2309
- };
2310
- }
2311
- try {
2312
- const response = await this.axiosInstance.post(`/content/${args.type}/list`, {
2313
- search: args.search || '',
2314
- filter: args.filter || {},
2315
- sort: args.sort || { key: 'meta.created', direction: 'desc', type: 'date' },
2316
- page: args.page || { size: 20, index: 1 },
2317
- select: args.select || [],
2318
- });
2319
- return {
2320
- content: [{
2321
- type: 'text',
2322
- text: JSON.stringify(response.data, null, 2),
2323
- }],
2324
- };
2325
- }
2326
- catch (error) {
2327
- return {
2328
- content: [{
2329
- type: 'text',
2330
- text: `Failed to list ${args.type} content: ${this.formatError(error)}`,
2331
- }],
2332
- isError: true,
2333
- };
2334
- }
2335
- }
2336
- async createContent(args) {
2337
- const validation = this.validateContentType(args.type);
2338
- if (!validation.valid) {
2339
- return {
2340
- content: [{
2341
- type: 'text',
2342
- text: validation.error,
2343
- }],
2344
- isError: true,
2345
- };
2346
- }
2347
- try {
2348
- // Create payload from args and filter out readOnly fields
2349
- let payload = { ...args };
2350
- payload = this.filterReadOnlyFields(payload, args.type);
2351
- // Handle scope inheritance for comment types
2352
- const isCommentType = args.type === 'comment' || args.type.includes('Comment');
2353
- if (isCommentType && payload.reference && (!payload.meta?.scopes || payload.meta.scopes.length === 0)) {
2354
- try {
2355
- // Fetch the referenced item to get its scopes
2356
- const referencedItem = await this.axiosInstance.get(`/content/${payload.reference}`);
2357
- if (referencedItem.data && referencedItem.data.meta && referencedItem.data.meta.scopes) {
2358
- if (!payload.meta)
2359
- payload.meta = {};
2360
- payload.meta.scopes = referencedItem.data.meta.scopes;
2361
- this.log(`Inherited scopes from referenced item ${payload.reference}: ${payload.meta.scopes.join(', ')}`);
2362
- }
2363
- }
2364
- catch (error) {
2365
- this.log(`Failed to fetch referenced item ${payload.reference} for scope inheritance: ${this.formatError(error)}`);
2366
- // Continue without scope inheritance if we can't fetch the referenced item
2367
- }
2368
- }
2369
- const response = await this.axiosInstance.post(`/content/${args.type}/create`, payload);
2370
- return {
2371
- content: [{
2372
- type: 'text',
2373
- text: `✅ **SUCCESSFULLY CREATED ${args.type.toUpperCase()} CONTENT**
2374
-
2375
- **Created Content:**
2376
- ${JSON.stringify(response.data, null, 2)}`,
2377
- }],
2378
- };
2379
- }
2380
- catch (error) {
2381
- // Pass through server errors directly - they contain the authoritative validation messages
2382
- return {
2383
- content: [{
2384
- type: 'text',
2385
- text: `Failed to create ${args.type}: ${this.formatError(error)}
2386
-
2387
- 💡 **Field Placement Guidance:**
2388
- - **Root Level**: title, reference, referenceType, body, organisation, meta
2389
- - **Data Object**: Custom fields defined in content type's "definedFields" array
2390
- - Use \`qik_get_content_definition\` to see the exact field structure for this content type`,
2391
- }],
2392
- isError: true,
2393
- };
2394
- }
2395
- }
2396
- async updateContent(args) {
2397
- try {
2398
- let updateData = { ...args.data };
2399
- // Get the content type for the item being updated
2400
- let contentType = null;
2401
42
  try {
2402
- const existingContent = await this.axiosInstance.get(`/content/${args.id}`);
2403
- if (existingContent.data && existingContent.data.meta && existingContent.data.meta.type) {
2404
- contentType = existingContent.data.meta.type;
2405
- }
43
+ const handler = getToolHandler(name);
44
+ const result = await handler(args);
45
+ return result;
2406
46
  }
2407
47
  catch (error) {
2408
- this.log(`Could not fetch existing content to determine type for readOnly filtering: ${this.formatError(error)}`);
48
+ throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error.message}`);
2409
49
  }
2410
- const method = args.replace ? 'put' : 'patch';
2411
- const response = await this.axiosInstance[method](`/content/${args.id}`, updateData);
2412
- return {
2413
- content: [{
2414
- type: 'text',
2415
- text: `✅ **SUCCESSFULLY UPDATED CONTENT**
2416
-
2417
- **Updated Content:**
2418
- ${JSON.stringify(response.data, null, 2)}`,
2419
- }],
2420
- };
2421
- }
2422
- catch (error) {
2423
- return {
2424
- content: [{
2425
- type: 'text',
2426
- text: `Failed to update content ${args.id}: ${this.formatError(error)}`,
2427
- }],
2428
- isError: true,
2429
- };
2430
- }
2431
- }
2432
- async deleteContent(args) {
2433
- try {
2434
- await this.axiosInstance.delete(`/content/${args.id}`);
2435
- return {
2436
- content: [{
2437
- type: 'text',
2438
- text: `Content ${args.id} deleted successfully`,
2439
- }],
2440
- };
2441
- }
2442
- catch (error) {
2443
- return {
2444
- content: [{
2445
- type: 'text',
2446
- text: `Failed to delete content ${args.id}: ${this.formatError(error)}`,
2447
- }],
2448
- isError: true,
2449
- };
2450
- }
2451
- }
2452
- async listProfiles(args) {
2453
- try {
2454
- const response = await this.axiosInstance.post('/content/profile/list', {
2455
- search: args.search || '',
2456
- filter: args.filter || {},
2457
- page: args.page || { size: 20, index: 1 },
2458
- });
2459
- return {
2460
- content: [{
2461
- type: 'text',
2462
- text: JSON.stringify(response.data, null, 2),
2463
- }],
2464
- };
2465
- }
2466
- catch (error) {
2467
- return {
2468
- content: [{
2469
- type: 'text',
2470
- text: `Failed to list profiles: ${this.formatError(error)}`,
2471
- }],
2472
- isError: true,
2473
- };
2474
- }
2475
- }
2476
- async createProfile(args) {
2477
- const payload = {
2478
- firstName: args.firstName,
2479
- lastName: args.lastName,
2480
- emails: args.emails || [],
2481
- phoneNumbers: args.phoneNumbers || [],
2482
- data: args.data || {},
2483
- meta: args.meta || {},
2484
- };
2485
- try {
2486
- const response = await this.axiosInstance.post('/content/profile/create', payload);
2487
- return {
2488
- content: [{
2489
- type: 'text',
2490
- text: JSON.stringify(response.data, null, 2),
2491
- }],
2492
- };
2493
- }
2494
- catch (error) {
2495
- return {
2496
- content: [{
2497
- type: 'text',
2498
- text: `Failed to create profile: ${this.formatError(error)}`,
2499
- }],
2500
- isError: true,
2501
- };
2502
- }
2503
- }
2504
- async getProfileTimeline(args) {
2505
- try {
2506
- const response = await this.axiosInstance.get(`/profile/${args.id}/timeline`);
2507
- return {
2508
- content: [{
2509
- type: 'text',
2510
- text: `📅 **PROFILE TIMELINE FOR ${args.id}**
2511
-
2512
- This timeline provides chronological activity data including recent actions, content created, events attended, workflow activities, and other activity logs.
2513
-
2514
- **Timeline Data:**
2515
- ${JSON.stringify(response.data, null, 2)}`,
2516
- }],
2517
- };
2518
- }
2519
- catch (error) {
2520
- return {
2521
- content: [{
2522
- type: 'text',
2523
- text: `Failed to get profile timeline for ${args.id}: ${this.formatError(error)}`,
2524
- }],
2525
- isError: true,
2526
- };
2527
- }
2528
- }
2529
- async getProfileInfo(args) {
2530
- const normalizedQuery = args.query.toLowerCase().trim();
2531
- // Analyze the query to determine intent
2532
- const timelineKeywords = [
2533
- 'activity', 'timeline', 'been up to', 'doing', 'recent', 'lately',
2534
- 'actions', 'history', 'events', 'workflow', 'progress'
2535
- ];
2536
- const basicInfoKeywords = [
2537
- 'contact', 'email', 'phone', 'details', 'information', 'about'
2538
- ];
2539
- const wantsTimeline = args.includeTimeline !== undefined
2540
- ? args.includeTimeline
2541
- : timelineKeywords.some(keyword => normalizedQuery.includes(keyword));
2542
- const wantsBasicInfo = basicInfoKeywords.some(keyword => normalizedQuery.includes(keyword));
2543
- // If the intent is ambiguous, ask for clarification
2544
- if (!wantsTimeline && !wantsBasicInfo && args.includeTimeline === undefined) {
2545
- const personName = args.profileName || (args.profileId ? `person (ID: ${args.profileId})` : 'this person');
2546
- return {
2547
- content: [{
2548
- type: 'text',
2549
- text: `🤔 **CLARIFICATION NEEDED**
2550
-
2551
- You asked: "${args.query}"
2552
-
2553
- I can provide different types of information about ${personName}:
2554
-
2555
- **OPTION 1: Basic Profile Details** 📋
2556
- - Contact information (email, phone)
2557
- - Basic demographic data
2558
- - Profile fields and custom data
2559
- → Use: \`qik_get_content\` or \`qik_list_profiles\`
2560
-
2561
- **OPTION 2: Activity Timeline** 📅
2562
- - Recent actions and activities
2563
- - Content they've created or been involved with
2564
- - Workflow progress and events attended
2565
- - Much richer, more colorful activity data
2566
- → Use: \`qik_get_profile_timeline\`
2567
-
2568
- **Which would be more helpful for your needs?**
2569
-
2570
- To get both, you can:
2571
- 1. First get basic profile info with \`qik_list_profiles\` (search by name)
2572
- 2. Then get timeline data with \`qik_get_profile_timeline\` (using the profile ID)`,
2573
- }],
2574
- };
2575
- }
2576
- let profileId = args.profileId;
2577
- // If no profile ID provided, try to find the profile by name
2578
- if (!profileId && args.profileName) {
2579
- try {
2580
- const searchResponse = await this.axiosInstance.post('/content/profile/list', {
2581
- search: args.profileName,
2582
- page: { size: 5, index: 1 },
2583
- });
2584
- if (searchResponse.data.items && searchResponse.data.items.length > 0) {
2585
- if (searchResponse.data.items.length === 1) {
2586
- profileId = searchResponse.data.items[0]._id;
2587
- }
2588
- else {
2589
- // Multiple matches found
2590
- const matches = searchResponse.data.items.map((p) => `- **${p._id}**: ${p.firstName} ${p.lastName} (${p.emails?.[0] || 'no email'})`).join('\n');
2591
- return {
2592
- content: [{
2593
- type: 'text',
2594
- text: `🔍 **MULTIPLE PROFILES FOUND**
2595
-
2596
- Found ${searchResponse.data.items.length} profiles matching "${args.profileName}":
2597
-
2598
- ${matches}
2599
-
2600
- Please specify which profile you want by using the profile ID with \`qik_get_profile_timeline\` or \`qik_get_content\`.`,
2601
- }],
2602
- };
2603
- }
2604
- }
2605
- else {
2606
- return {
2607
- content: [{
2608
- type: 'text',
2609
- text: `❌ **PROFILE NOT FOUND**
2610
-
2611
- No profiles found matching "${args.profileName}".
2612
-
2613
- Try:
2614
- - Using \`qik_list_profiles\` with a broader search
2615
- - Checking the spelling of the name
2616
- - Using the exact profile ID if you have it`,
2617
- }],
2618
- isError: true,
2619
- };
2620
- }
2621
- }
2622
- catch (error) {
2623
- return {
2624
- content: [{
2625
- type: 'text',
2626
- text: `Failed to search for profile "${args.profileName}": ${this.formatError(error)}`,
2627
- }],
2628
- isError: true,
2629
- };
2630
- }
2631
- }
2632
- // Now we have a profile ID, get the appropriate information
2633
- if (wantsTimeline && profileId) {
2634
- return await this.getProfileTimeline({ id: profileId });
2635
- }
2636
- else if (profileId) {
2637
- // Get basic profile information
2638
- try {
2639
- const response = await this.axiosInstance.get(`/content/${profileId}`);
2640
- return {
2641
- content: [{
2642
- type: 'text',
2643
- text: `👤 **PROFILE INFORMATION**
2644
-
2645
- **Basic Details:**
2646
- ${JSON.stringify(response.data, null, 2)}
2647
-
2648
- 💡 **Want more activity details?** Use \`qik_get_profile_timeline\` with ID: ${profileId} to see their recent activities, workflow progress, and timeline data.`,
2649
- }],
2650
- };
2651
- }
2652
- catch (error) {
2653
- return {
2654
- content: [{
2655
- type: 'text',
2656
- text: `Failed to get profile information for ${profileId}: ${this.formatError(error)}`,
2657
- }],
2658
- isError: true,
2659
- };
2660
- }
2661
- }
2662
- else {
2663
- return {
2664
- content: [{
2665
- type: 'text',
2666
- text: `❌ **MISSING PROFILE IDENTIFIER**
2667
-
2668
- To get profile information, I need either:
2669
- - **profileId**: The exact profile ID
2670
- - **profileName**: The person's name to search for
2671
-
2672
- Please provide one of these and try again.`,
2673
- }],
2674
- isError: true,
2675
- };
2676
- }
2677
- }
2678
- async getForm(args) {
2679
- try {
2680
- const response = await this.axiosInstance.get(`/form/${args.id}`);
2681
- return {
2682
- content: [{
2683
- type: 'text',
2684
- text: JSON.stringify(response.data, null, 2),
2685
- }],
2686
- };
2687
- }
2688
- catch (error) {
2689
- return {
2690
- content: [{
2691
- type: 'text',
2692
- text: `Failed to get form ${args.id}: ${this.formatError(error)}`,
2693
- }],
2694
- isError: true,
2695
- };
2696
- }
2697
- }
2698
- async submitForm(args) {
2699
- try {
2700
- const response = await this.axiosInstance.post(`/form/${args.id}`, args.data);
2701
- return {
2702
- content: [{
2703
- type: 'text',
2704
- text: JSON.stringify(response.data, null, 2),
2705
- }],
2706
- };
2707
- }
2708
- catch (error) {
2709
- return {
2710
- content: [{
2711
- type: 'text',
2712
- text: `Failed to submit form ${args.id}: ${this.formatError(error)}`,
2713
- }],
2714
- isError: true,
2715
- };
2716
- }
2717
- }
2718
- async uploadFile(args) {
2719
- try {
2720
- const formData = new FormData();
2721
- // Convert base64 to buffer
2722
- const fileBuffer = Buffer.from(args.fileData, 'base64');
2723
- formData.append('file', fileBuffer, args.fileName);
2724
- const jsonData = {
2725
- title: args.title,
2726
- meta: args.meta || {},
2727
- };
2728
- formData.append('json', JSON.stringify(jsonData));
2729
- const response = await this.axiosInstance.post('/file/upload', formData, {
2730
- headers: {
2731
- ...formData.getHeaders(),
2732
- 'Authorization': `Bearer ${QIK_ACCESS_TOKEN}`,
2733
- },
2734
- });
2735
- return {
2736
- content: [{
2737
- type: 'text',
2738
- text: JSON.stringify(response.data, null, 2),
2739
- }],
2740
- };
2741
- }
2742
- catch (error) {
2743
- return {
2744
- content: [{
2745
- type: 'text',
2746
- text: `Failed to upload file: ${this.formatError(error)}`,
2747
- }],
2748
- isError: true,
2749
- };
2750
- }
2751
- }
2752
- async searchContent(args) {
2753
- const results = [];
2754
- const types = args.types || Object.keys(this.glossary);
2755
- // Limit to 5 types to avoid too many requests
2756
- for (const type of types.slice(0, 5)) {
2757
- try {
2758
- const response = await this.axiosInstance.post(`/content/${type}/list`, {
2759
- search: args.query,
2760
- page: { size: args.limit || 20, index: 1 },
2761
- });
2762
- if (response.data.items && response.data.items.length > 0) {
2763
- results.push({
2764
- type,
2765
- items: response.data.items,
2766
- total: response.data.total,
2767
- });
2768
- }
2769
- }
2770
- catch (error) {
2771
- // Skip types that error (might not exist or no permission)
2772
- continue;
2773
- }
2774
- }
2775
- return {
2776
- content: [{
2777
- type: 'text',
2778
- text: JSON.stringify(results, null, 2),
2779
- }],
2780
- };
2781
- }
2782
- async getScopes() {
2783
- try {
2784
- const response = await this.axiosInstance.get('/scope/tree');
2785
- return {
2786
- content: [{
2787
- type: 'text',
2788
- text: JSON.stringify(response.data, null, 2),
2789
- }],
2790
- };
2791
- }
2792
- catch (error) {
2793
- return {
2794
- content: [{
2795
- type: 'text',
2796
- text: `Failed to get scopes: ${this.formatError(error)}`,
2797
- }],
2798
- isError: true,
2799
- };
2800
- }
2801
- }
2802
- async getSmartlist(args) {
2803
- try {
2804
- const response = await this.axiosInstance.get(`/smartlist/${args.id}`);
2805
- return {
2806
- content: [{
2807
- type: 'text',
2808
- text: JSON.stringify(response.data, null, 2),
2809
- }],
2810
- };
2811
- }
2812
- catch (error) {
2813
- return {
2814
- content: [{
2815
- type: 'text',
2816
- text: `Failed to get smartlist ${args.id}: ${this.formatError(error)}`,
2817
- }],
2818
- isError: true,
2819
- };
2820
- }
2821
- }
2822
- // Documentation-focused tool implementations
2823
- async getDocumentation(args) {
2824
- try {
2825
- const documentation = QikDocumentationHelper.getTopicDocumentation(args.topic);
2826
- if (!documentation) {
2827
- return {
2828
- content: [{
2829
- type: 'text',
2830
- text: `❌ **DOCUMENTATION NOT FOUND**
2831
-
2832
- No documentation found for topic: "${args.topic}"
2833
-
2834
- **Available topics:**
2835
- - authentication (token types, methods, error codes)
2836
- - endpoints (API endpoint documentation)
2837
- - contentTypes (content type specific guidance)
2838
- - filterSyntax (filter operators and comparators)
2839
- - concepts (key concepts like field placement, scopes, workflows)
2840
- - examples (code examples for common use cases)
2841
- - troubleshooting (error resolution guides)
2842
-
2843
- **Example usage:**
2844
- - \`qik_get_documentation\` with topic: "concepts.field_placement"
2845
- - \`qik_get_documentation\` with topic: "authentication.tokenTypes"
2846
- - \`qik_get_documentation\` with topic: "troubleshooting.field_placement_errors"`,
2847
- }],
2848
- isError: true,
2849
- };
2850
- }
2851
- let result = `📖 **DOCUMENTATION: ${args.topic.toUpperCase()}**\n\n`;
2852
- if (args.subtopic && documentation[args.subtopic]) {
2853
- result += `**Subtopic: ${args.subtopic}**\n\n`;
2854
- result += JSON.stringify(documentation[args.subtopic], null, 2);
2855
- }
2856
- else {
2857
- result += JSON.stringify(documentation, null, 2);
2858
- }
2859
- return {
2860
- content: [{
2861
- type: 'text',
2862
- text: result,
2863
- }],
2864
- };
2865
- }
2866
- catch (error) {
2867
- return {
2868
- content: [{
2869
- type: 'text',
2870
- text: `Failed to get documentation for ${args.topic}: ${this.formatError(error)}`,
2871
- }],
2872
- isError: true,
2873
- };
2874
- }
2875
- }
2876
- async searchDocumentation(args) {
2877
- try {
2878
- const results = QikDocumentationHelper.searchDocumentation(args.query);
2879
- const limit = args.limit || 10;
2880
- const limitedResults = results.slice(0, limit);
2881
- if (limitedResults.length === 0) {
2882
- return {
2883
- content: [{
2884
- type: 'text',
2885
- text: `🔍 **NO DOCUMENTATION FOUND**
2886
-
2887
- No documentation found for query: "${args.query}"
2888
-
2889
- **Try searching for:**
2890
- - "field placement" - for field structure guidance
2891
- - "authentication" - for token and auth help
2892
- - "filter" - for filtering and query syntax
2893
- - "workflow" - for workflow system concepts
2894
- - "birthday" - for anniversary/birthday queries
2895
- - "scope" - for permission and scope guidance
2896
- - "troubleshooting" - for error resolution
2897
-
2898
- **Available documentation sections:**
2899
- - Authentication & Tokens
2900
- - API Endpoints
2901
- - Content Types
2902
- - Filter Syntax
2903
- - Core Concepts
2904
- - Code Examples
2905
- - Troubleshooting Guides`,
2906
- }],
2907
- };
2908
- }
2909
- let result = `🔍 **DOCUMENTATION SEARCH RESULTS**\n\n`;
2910
- result += `Found ${limitedResults.length} results for "${args.query}":\n\n`;
2911
- limitedResults.forEach((item, index) => {
2912
- result += `**${index + 1}. ${item.path}** (${item.type})\n`;
2913
- if (typeof item.content === 'string') {
2914
- const preview = item.content.length > 200
2915
- ? item.content.substring(0, 200) + '...'
2916
- : item.content;
2917
- result += `${preview}\n\n`;
2918
- }
2919
- else {
2920
- result += `${JSON.stringify(item.content).substring(0, 200)}...\n\n`;
2921
- }
2922
- });
2923
- result += `💡 **Need more specific help?** Use \`qik_get_documentation\` with a specific topic for detailed information.`;
2924
- return {
2925
- content: [{
2926
- type: 'text',
2927
- text: result,
2928
- }],
2929
- };
2930
- }
2931
- catch (error) {
2932
- return {
2933
- content: [{
2934
- type: 'text',
2935
- text: `Failed to search documentation: ${this.formatError(error)}`,
2936
- }],
2937
- isError: true,
2938
- };
2939
- }
2940
- }
2941
- async getTroubleshooting(args) {
2942
- try {
2943
- let troubleshootingInfo;
2944
- if (args.issueType) {
2945
- // Get specific issue type
2946
- troubleshootingInfo = QIK_DOCUMENTATION.troubleshooting[args.issueType];
2947
- if (troubleshootingInfo) {
2948
- troubleshootingInfo = { issue: args.issueType, ...troubleshootingInfo };
2949
- }
2950
- }
2951
- else {
2952
- // Auto-detect issue type from error message
2953
- troubleshootingInfo = QikDocumentationHelper.getTroubleshootingInfo(args.errorMessage);
2954
- }
2955
- if (!troubleshootingInfo) {
2956
- return {
2957
- content: [{
2958
- type: 'text',
2959
- text: `🔧 **NO SPECIFIC TROUBLESHOOTING FOUND**
2960
-
2961
- I couldn't find specific troubleshooting information for: "${args.errorMessage}"
2962
-
2963
- **Available troubleshooting categories:**
2964
- - **field_placement_errors**: Field structure and placement issues
2965
- - **authentication_failures**: Token and permission problems
2966
- - **filter_syntax_errors**: Query and filter syntax issues
2967
- - **scope_permission_issues**: Access and permission problems
2968
- - **workflow_confusion**: Workflow definition vs card confusion
2969
- - **date_handling_issues**: Date format and timezone problems
2970
- - **performance_issues**: Slow queries and optimization
2971
-
2972
- **General troubleshooting steps:**
2973
- 1. Check the error message for specific field names or codes
2974
- 2. Verify your access token and permissions
2975
- 3. Ensure proper field placement (root vs data object)
2976
- 4. Validate filter syntax and comparators
2977
- 5. Check scope permissions and hierarchy
2978
-
2979
- **Need more help?** Try:
2980
- - \`qik_search_documentation\` with keywords from your error
2981
- - \`qik_get_documentation\` with topic: "troubleshooting"
2982
- - \`qik_validate_field_placement\` for content creation issues`,
2983
- }],
2984
- };
2985
- }
2986
- let result = `🔧 **TROUBLESHOOTING: ${troubleshootingInfo.issue.toUpperCase().replace(/_/g, ' ')}**\n\n`;
2987
- result += `**Your Error:** "${args.errorMessage}"\n\n`;
2988
- if (troubleshootingInfo.symptoms && troubleshootingInfo.symptoms.length > 0) {
2989
- result += `**Common Symptoms:**\n`;
2990
- result += troubleshootingInfo.symptoms.map((s) => `- ${s}`).join('\n');
2991
- result += '\n\n';
2992
- }
2993
- if (troubleshootingInfo.causes && troubleshootingInfo.causes.length > 0) {
2994
- result += `**Likely Causes:**\n`;
2995
- result += troubleshootingInfo.causes.map((c) => `- ${c}`).join('\n');
2996
- result += '\n\n';
2997
- }
2998
- if (troubleshootingInfo.solutions && troubleshootingInfo.solutions.length > 0) {
2999
- result += `**Solutions:**\n`;
3000
- result += troubleshootingInfo.solutions.map((s) => `- ${s}`).join('\n');
3001
- result += '\n\n';
3002
- }
3003
- if (troubleshootingInfo.prevention && troubleshootingInfo.prevention.length > 0) {
3004
- result += `**Prevention:**\n`;
3005
- result += troubleshootingInfo.prevention.map((p) => `- ${p}`).join('\n');
3006
- result += '\n\n';
3007
- }
3008
- if (troubleshootingInfo.relatedIssues && troubleshootingInfo.relatedIssues.length > 0) {
3009
- result += `**Related Issues:**\n`;
3010
- result += troubleshootingInfo.relatedIssues.map((r) => `- ${r}`).join('\n');
3011
- }
3012
- return {
3013
- content: [{
3014
- type: 'text',
3015
- text: result,
3016
- }],
3017
- };
3018
- }
3019
- catch (error) {
3020
- return {
3021
- content: [{
3022
- type: 'text',
3023
- text: `Failed to get troubleshooting information: ${this.formatError(error)}`,
3024
- }],
3025
- isError: true,
3026
- };
3027
- }
3028
- }
3029
- async getExamples(args) {
3030
- try {
3031
- if (args.category === 'all') {
3032
- let result = `💡 **ALL CODE EXAMPLES**\n\n`;
3033
- for (const [category, examples] of Object.entries(QIK_DOCUMENTATION.examples)) {
3034
- result += `## ${category.toUpperCase()}\n\n`;
3035
- for (const [exampleKey, example] of Object.entries(examples)) {
3036
- const typedExample = example;
3037
- result += `### ${typedExample.title}\n`;
3038
- result += `${typedExample.description}\n\n`;
3039
- result += `\`\`\`json\n${JSON.stringify(typedExample.code, null, 2)}\n\`\`\`\n\n`;
3040
- result += `**Explanation:** ${typedExample.explanation}\n\n`;
3041
- if (typedExample.variations && typedExample.variations.length > 0) {
3042
- result += `**Variations:**\n`;
3043
- typedExample.variations.forEach((variation) => {
3044
- result += `- ${variation.description}\n`;
3045
- result += ` \`\`\`json\n ${JSON.stringify(variation.code, null, 2)}\n \`\`\`\n`;
3046
- });
3047
- result += '\n';
3048
- }
3049
- }
3050
- }
3051
- return {
3052
- content: [{
3053
- type: 'text',
3054
- text: result,
3055
- }],
3056
- };
3057
- }
3058
- const examples = QikDocumentationHelper.getExamples(args.category, args.example);
3059
- if (!examples) {
3060
- return {
3061
- content: [{
3062
- type: 'text',
3063
- text: `💡 **NO EXAMPLES FOUND**
3064
-
3065
- No examples found for category: "${args.category}"
3066
-
3067
- **Available example categories:**
3068
- - **authentication**: Token usage and auth examples
3069
- - **content_creation**: Creating different content types
3070
- - **filtering**: Advanced filtering and search examples
3071
-
3072
- **Example usage:**
3073
- - \`qik_get_examples\` with category: "authentication"
3074
- - \`qik_get_examples\` with category: "filtering" and example: "birthday_search"
3075
- - \`qik_get_examples\` with category: "all" (shows all examples)`,
3076
- }],
3077
- isError: true,
3078
- };
3079
- }
3080
- let result = `💡 **CODE EXAMPLES: ${args.category.toUpperCase()}**\n\n`;
3081
- if (args.example && examples[args.example]) {
3082
- // Show specific example
3083
- const example = examples[args.example];
3084
- result += `### ${example.title}\n`;
3085
- result += `${example.description}\n\n`;
3086
- result += `\`\`\`json\n${JSON.stringify(example.code, null, 2)}\n\`\`\`\n\n`;
3087
- result += `**Explanation:** ${example.explanation}\n\n`;
3088
- if (example.variations && example.variations.length > 0) {
3089
- result += `**Variations:**\n`;
3090
- example.variations.forEach((variation) => {
3091
- result += `- ${variation.description}\n`;
3092
- result += ` \`\`\`json\n ${JSON.stringify(variation.code, null, 2)}\n \`\`\`\n`;
3093
- });
3094
- }
3095
- }
3096
- else {
3097
- // Show all examples in category
3098
- for (const [exampleKey, example] of Object.entries(examples)) {
3099
- const typedExample = example;
3100
- result += `### ${typedExample.title}\n`;
3101
- result += `${typedExample.description}\n\n`;
3102
- result += `\`\`\`json\n${JSON.stringify(typedExample.code, null, 2)}\n\`\`\`\n\n`;
3103
- result += `**Explanation:** ${typedExample.explanation}\n\n`;
3104
- if (typedExample.variations && typedExample.variations.length > 0) {
3105
- result += `**Variations:**\n`;
3106
- typedExample.variations.forEach((variation) => {
3107
- result += `- ${variation.description}\n`;
3108
- result += ` \`\`\`json\n ${JSON.stringify(variation.code, null, 2)}\n \`\`\`\n`;
3109
- });
3110
- result += '\n';
3111
- }
3112
- }
3113
- }
3114
- return {
3115
- content: [{
3116
- type: 'text',
3117
- text: result,
3118
- }],
3119
- };
3120
- }
3121
- catch (error) {
3122
- return {
3123
- content: [{
3124
- type: 'text',
3125
- text: `Failed to get examples: ${this.formatError(error)}`,
3126
- }],
3127
- isError: true,
3128
- };
3129
- }
3130
- }
3131
- async validateFieldPlacement(args) {
3132
- try {
3133
- const validation = this.validateContentType(args.contentType);
3134
- if (!validation.valid) {
3135
- return {
3136
- content: [{
3137
- type: 'text',
3138
- text: validation.error,
3139
- }],
3140
- isError: true,
3141
- };
3142
- }
3143
- // Simplified validation using documentation helper only
3144
- const docValidation = QikDocumentationHelper.validateFieldPlacement(args.contentType, args.payload);
3145
- let result = `✅ **FIELD PLACEMENT VALIDATION: ${args.contentType.toUpperCase()}**\n\n`;
3146
- if (docValidation.valid) {
3147
- result += `🎉 **VALIDATION PASSED**\n\n`;
3148
- result += `Your payload has correct field placement!\n\n`;
3149
- result += `**Field Analysis:**\n`;
3150
- result += `- Root level fields: ${Object.keys(args.payload).filter(k => k !== 'data' && k !== 'meta').join(', ') || 'none'}\n`;
3151
- result += `- Data object fields: ${args.payload.data ? Object.keys(args.payload.data).join(', ') : 'none'}\n`;
3152
- result += `- Meta fields: ${args.payload.meta ? Object.keys(args.payload.meta).join(', ') : 'none'}\n\n`;
3153
- }
3154
- else {
3155
- result += `❌ **VALIDATION FAILED**\n\n`;
3156
- if (docValidation.errors && docValidation.errors.length > 0) {
3157
- result += `**Errors Found:**\n`;
3158
- docValidation.errors.forEach(error => {
3159
- result += `- ${error}\n`;
3160
- });
3161
- result += '\n';
3162
- }
3163
- result += `**General Field Placement Rules:**\n`;
3164
- result += `- **Root Level**: title, reference, referenceType, body, organisation, meta\n`;
3165
- result += `- **Data Object**: Custom fields defined in content type's "definedFields" array\n`;
3166
- result += `- Use \`qik_get_content_definition\` to see the exact field structure for this content type\n\n`;
3167
- result += `**Correct Structure Example:**\n`;
3168
- result += `\`\`\`json\n`;
3169
- result += `{\n`;
3170
- result += ` "type": "${args.contentType}",\n`;
3171
- result += ` "title": "Your Title Here",\n`;
3172
- result += ` "meta": { "scopes": ["scope-id-here"] },\n`;
3173
- result += ` "data": {\n`;
3174
- result += ` "customField": "value"\n`;
3175
- result += ` }\n`;
3176
- result += `}\n\`\`\`\n`;
3177
- }
3178
- return {
3179
- content: [{
3180
- type: 'text',
3181
- text: result,
3182
- }],
3183
- isError: !docValidation.valid,
3184
- };
3185
- }
3186
- catch (error) {
3187
- return {
3188
- content: [{
3189
- type: 'text',
3190
- text: `Failed to validate field placement: ${this.formatError(error)}`,
3191
- }],
3192
- isError: true,
3193
- };
3194
- }
50
+ });
3195
51
  }
3196
52
  async run() {
3197
53
  const transport = new StdioServerTransport();
3198
54
  await this.server.connect(transport);
3199
- this.log('Qik MCP server running on stdio');
55
+ console.error('Qik MCP server running on stdio');
3200
56
  }
3201
57
  }
3202
- // Only run the server if this file is executed directly (not imported)
58
+ // Only run the server if this file is executed directly
3203
59
  if (import.meta.url === `file://${process.argv[1]}`) {
3204
60
  const server = new QikMCPServer();
3205
61
  server.run().catch(console.error);