@qikdev/mcp 1.0.0

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.
@@ -0,0 +1,731 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Qik Platform MCP Server
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
+ * Features:
10
+ * - Content management (CRUD operations)
11
+ * - Content type discovery via glossary
12
+ * - User and profile management
13
+ * - Form handling and submissions
14
+ * - File upload and management
15
+ * - Search and filtering capabilities
16
+ * - Scope and permission management
17
+ */
18
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
21
+ import axios from 'axios';
22
+ import FormData from 'form-data';
23
+ // Environment variables
24
+ const QIK_API_URL = process.env.QIK_API_URL || 'https://api.qik.dev';
25
+ const QIK_ACCESS_TOKEN = process.env.QIK_ACCESS_TOKEN;
26
+ export class QikMCPServer {
27
+ server;
28
+ axiosInstance;
29
+ glossary = {};
30
+ constructor() {
31
+ if (!QIK_ACCESS_TOKEN) {
32
+ throw new Error('QIK_ACCESS_TOKEN environment variable is required. Run "qik-mcp-server setup" to configure.');
33
+ }
34
+ this.server = new Server({
35
+ name: "qik-mcp-server",
36
+ version: "1.0.0",
37
+ }, {
38
+ capabilities: {
39
+ tools: {},
40
+ },
41
+ });
42
+ // Configure axios instance with Qik API settings
43
+ this.axiosInstance = axios.create({
44
+ baseURL: QIK_API_URL,
45
+ headers: {
46
+ 'Authorization': `Bearer ${QIK_ACCESS_TOKEN}`,
47
+ 'Content-Type': 'application/json',
48
+ },
49
+ timeout: 30000,
50
+ });
51
+ this.setupToolHandlers();
52
+ this.initializeGlossary();
53
+ // Error handling
54
+ this.server.onerror = (error) => console.error('[MCP Error]', error);
55
+ process.on('SIGINT', async () => {
56
+ await this.server.close();
57
+ process.exit(0);
58
+ });
59
+ }
60
+ async initializeGlossary() {
61
+ try {
62
+ const response = await this.axiosInstance.get('/glossary');
63
+ this.glossary = response.data || {};
64
+ console.error(`[Qik MCP] Loaded ${Object.keys(this.glossary).length} content types from glossary`);
65
+ }
66
+ catch (error) {
67
+ console.error('[Qik MCP] Failed to load glossary:', this.formatError(error));
68
+ }
69
+ }
70
+ formatError(error) {
71
+ if (axios.isAxiosError(error)) {
72
+ const axiosError = error;
73
+ if (axiosError.response) {
74
+ return `HTTP ${axiosError.response.status}: ${JSON.stringify(axiosError.response.data)}`;
75
+ }
76
+ return `Network error: ${axiosError.message}`;
77
+ }
78
+ return error.message || String(error);
79
+ }
80
+ setupToolHandlers() {
81
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
82
+ tools: [
83
+ // Authentication & Session
84
+ {
85
+ name: 'qik_get_user_session',
86
+ description: 'Get current user session information',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: {},
90
+ },
91
+ },
92
+ // Content Type Discovery
93
+ {
94
+ name: 'qik_get_glossary',
95
+ description: 'Get all available content types and their definitions',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {},
99
+ },
100
+ },
101
+ {
102
+ name: 'qik_get_content_definition',
103
+ description: 'Get definition for a specific content type',
104
+ inputSchema: {
105
+ type: 'object',
106
+ properties: {
107
+ type: {
108
+ type: 'string',
109
+ description: 'Content type key (e.g., "article", "profile", "event")',
110
+ },
111
+ },
112
+ required: ['type'],
113
+ },
114
+ },
115
+ // Content Management
116
+ {
117
+ name: 'qik_get_content',
118
+ description: 'Get content item by ID or slug',
119
+ inputSchema: {
120
+ type: 'object',
121
+ properties: {
122
+ id: {
123
+ type: 'string',
124
+ description: 'Content ID',
125
+ },
126
+ slug: {
127
+ type: 'string',
128
+ description: 'Content slug (e.g., "article:my-post" or "car:pathfinder")',
129
+ },
130
+ },
131
+ oneOf: [
132
+ { required: ['id'] },
133
+ { required: ['slug'] },
134
+ ],
135
+ },
136
+ },
137
+ {
138
+ name: 'qik_list_content',
139
+ description: 'List content items with filtering and search',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ type: {
144
+ type: 'string',
145
+ description: 'Content type to list (e.g., "article", "profile", "event")',
146
+ },
147
+ search: {
148
+ type: 'string',
149
+ description: 'Search keywords',
150
+ },
151
+ filter: {
152
+ type: 'object',
153
+ description: 'Filter criteria using Qik filter syntax',
154
+ },
155
+ sort: {
156
+ type: 'object',
157
+ properties: {
158
+ key: { type: 'string' },
159
+ direction: { type: 'string', enum: ['asc', 'desc'] },
160
+ type: { type: 'string', enum: ['string', 'number', 'date'] },
161
+ },
162
+ },
163
+ page: {
164
+ type: 'object',
165
+ properties: {
166
+ size: { type: 'number', minimum: 1, maximum: 100 },
167
+ index: { type: 'number', minimum: 1 },
168
+ },
169
+ },
170
+ select: {
171
+ type: 'array',
172
+ items: { type: 'string' },
173
+ description: 'Fields to include in response',
174
+ },
175
+ },
176
+ required: ['type'],
177
+ },
178
+ },
179
+ {
180
+ name: 'qik_create_content',
181
+ description: 'Create new content item',
182
+ inputSchema: {
183
+ type: 'object',
184
+ properties: {
185
+ type: {
186
+ type: 'string',
187
+ description: 'Content type to create',
188
+ },
189
+ title: {
190
+ type: 'string',
191
+ description: 'Content title',
192
+ },
193
+ data: {
194
+ type: 'object',
195
+ description: 'Content data fields',
196
+ },
197
+ meta: {
198
+ type: 'object',
199
+ description: 'Meta information (scopes, tags, etc.)',
200
+ },
201
+ },
202
+ required: ['type', 'title'],
203
+ },
204
+ },
205
+ {
206
+ name: 'qik_update_content',
207
+ description: 'Update existing content item',
208
+ inputSchema: {
209
+ type: 'object',
210
+ properties: {
211
+ id: {
212
+ type: 'string',
213
+ description: 'Content ID to update',
214
+ },
215
+ data: {
216
+ type: 'object',
217
+ description: 'Data to update (partial update)',
218
+ },
219
+ replace: {
220
+ type: 'boolean',
221
+ description: 'Whether to replace entire content (PUT) or merge (PATCH)',
222
+ default: false,
223
+ },
224
+ },
225
+ required: ['id', 'data'],
226
+ },
227
+ },
228
+ {
229
+ name: 'qik_delete_content',
230
+ description: 'Delete content item',
231
+ inputSchema: {
232
+ type: 'object',
233
+ properties: {
234
+ id: {
235
+ type: 'string',
236
+ description: 'Content ID to delete',
237
+ },
238
+ },
239
+ required: ['id'],
240
+ },
241
+ },
242
+ // Profile Management
243
+ {
244
+ name: 'qik_list_profiles',
245
+ description: 'Search and list profiles/people',
246
+ inputSchema: {
247
+ type: 'object',
248
+ properties: {
249
+ search: {
250
+ type: 'string',
251
+ description: 'Search by name or email',
252
+ },
253
+ filter: {
254
+ type: 'object',
255
+ description: 'Filter criteria',
256
+ },
257
+ page: {
258
+ type: 'object',
259
+ properties: {
260
+ size: { type: 'number', minimum: 1, maximum: 100 },
261
+ index: { type: 'number', minimum: 1 },
262
+ },
263
+ },
264
+ },
265
+ },
266
+ },
267
+ {
268
+ name: 'qik_create_profile',
269
+ description: 'Create new profile/person',
270
+ inputSchema: {
271
+ type: 'object',
272
+ properties: {
273
+ firstName: {
274
+ type: 'string',
275
+ description: 'First name',
276
+ },
277
+ lastName: {
278
+ type: 'string',
279
+ description: 'Last name',
280
+ },
281
+ emails: {
282
+ type: 'array',
283
+ items: { type: 'string' },
284
+ description: 'Email addresses',
285
+ },
286
+ phoneNumbers: {
287
+ type: 'array',
288
+ items: {
289
+ type: 'object',
290
+ properties: {
291
+ label: { type: 'string' },
292
+ countryCode: { type: 'string' },
293
+ number: { type: 'string' },
294
+ },
295
+ },
296
+ description: 'Phone numbers',
297
+ },
298
+ data: {
299
+ type: 'object',
300
+ description: 'Additional profile data',
301
+ },
302
+ meta: {
303
+ type: 'object',
304
+ description: 'Meta information (scopes, tags, etc.)',
305
+ },
306
+ },
307
+ required: ['firstName', 'lastName'],
308
+ },
309
+ },
310
+ // Form Management
311
+ {
312
+ name: 'qik_get_form',
313
+ description: 'Get form definition',
314
+ inputSchema: {
315
+ type: 'object',
316
+ properties: {
317
+ id: {
318
+ type: 'string',
319
+ description: 'Form ID',
320
+ },
321
+ },
322
+ required: ['id'],
323
+ },
324
+ },
325
+ {
326
+ name: 'qik_submit_form',
327
+ description: 'Submit form data',
328
+ inputSchema: {
329
+ type: 'object',
330
+ properties: {
331
+ id: {
332
+ type: 'string',
333
+ description: 'Form ID',
334
+ },
335
+ data: {
336
+ type: 'object',
337
+ description: 'Form submission data',
338
+ },
339
+ },
340
+ required: ['id', 'data'],
341
+ },
342
+ },
343
+ // File Management
344
+ {
345
+ name: 'qik_upload_file',
346
+ description: 'Upload file to Qik',
347
+ inputSchema: {
348
+ type: 'object',
349
+ properties: {
350
+ title: {
351
+ type: 'string',
352
+ description: 'File title',
353
+ },
354
+ fileData: {
355
+ type: 'string',
356
+ description: 'Base64 encoded file data',
357
+ },
358
+ fileName: {
359
+ type: 'string',
360
+ description: 'Original file name',
361
+ },
362
+ mimeType: {
363
+ type: 'string',
364
+ description: 'File MIME type',
365
+ },
366
+ meta: {
367
+ type: 'object',
368
+ description: 'Meta information (scopes, tags, etc.)',
369
+ },
370
+ },
371
+ required: ['title', 'fileData', 'fileName'],
372
+ },
373
+ },
374
+ // Search & Discovery
375
+ {
376
+ name: 'qik_search_content',
377
+ description: 'Global content search across all types',
378
+ inputSchema: {
379
+ type: 'object',
380
+ properties: {
381
+ query: {
382
+ type: 'string',
383
+ description: 'Search query',
384
+ },
385
+ types: {
386
+ type: 'array',
387
+ items: { type: 'string' },
388
+ description: 'Content types to search in',
389
+ },
390
+ limit: {
391
+ type: 'number',
392
+ minimum: 1,
393
+ maximum: 100,
394
+ default: 20,
395
+ },
396
+ },
397
+ required: ['query'],
398
+ },
399
+ },
400
+ {
401
+ name: 'qik_get_scopes',
402
+ description: 'Get available scopes/permissions tree',
403
+ inputSchema: {
404
+ type: 'object',
405
+ properties: {},
406
+ },
407
+ },
408
+ // Utility Tools
409
+ {
410
+ name: 'qik_get_smartlist',
411
+ description: 'Execute a smartlist query',
412
+ inputSchema: {
413
+ type: 'object',
414
+ properties: {
415
+ id: {
416
+ type: 'string',
417
+ description: 'Smartlist ID',
418
+ },
419
+ },
420
+ required: ['id'],
421
+ },
422
+ },
423
+ ],
424
+ }));
425
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
426
+ try {
427
+ switch (request.params.name) {
428
+ case 'qik_get_user_session':
429
+ return await this.getUserSession();
430
+ case 'qik_get_glossary':
431
+ return await this.getGlossary();
432
+ case 'qik_get_content_definition':
433
+ return await this.getContentDefinition(request.params.arguments);
434
+ case 'qik_get_content':
435
+ return await this.getContent(request.params.arguments);
436
+ case 'qik_list_content':
437
+ return await this.listContent(request.params.arguments);
438
+ case 'qik_create_content':
439
+ return await this.createContent(request.params.arguments);
440
+ case 'qik_update_content':
441
+ return await this.updateContent(request.params.arguments);
442
+ case 'qik_delete_content':
443
+ return await this.deleteContent(request.params.arguments);
444
+ case 'qik_list_profiles':
445
+ return await this.listProfiles(request.params.arguments);
446
+ case 'qik_create_profile':
447
+ return await this.createProfile(request.params.arguments);
448
+ case 'qik_get_form':
449
+ return await this.getForm(request.params.arguments);
450
+ case 'qik_submit_form':
451
+ return await this.submitForm(request.params.arguments);
452
+ case 'qik_upload_file':
453
+ return await this.uploadFile(request.params.arguments);
454
+ case 'qik_search_content':
455
+ return await this.searchContent(request.params.arguments);
456
+ case 'qik_get_scopes':
457
+ return await this.getScopes();
458
+ case 'qik_get_smartlist':
459
+ return await this.getSmartlist(request.params.arguments);
460
+ default:
461
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
462
+ }
463
+ }
464
+ catch (error) {
465
+ console.error(`[Qik MCP] Error in ${request.params.name}:`, error);
466
+ if (axios.isAxiosError(error)) {
467
+ const axiosError = error;
468
+ if (axiosError.response?.status === 401) {
469
+ return {
470
+ content: [{
471
+ type: 'text',
472
+ text: 'Authentication failed. Please check your access token configuration.',
473
+ }],
474
+ isError: true,
475
+ };
476
+ }
477
+ else if (axiosError.response?.status === 403) {
478
+ return {
479
+ content: [{
480
+ type: 'text',
481
+ text: 'Access denied. Your token may not have permission for this operation.',
482
+ }],
483
+ isError: true,
484
+ };
485
+ }
486
+ else if (axiosError.response?.status === 429) {
487
+ return {
488
+ content: [{
489
+ type: 'text',
490
+ text: 'Rate limit exceeded. Please try again later.',
491
+ }],
492
+ isError: true,
493
+ };
494
+ }
495
+ }
496
+ return {
497
+ content: [{
498
+ type: 'text',
499
+ text: `Error: ${this.formatError(error)}`,
500
+ }],
501
+ isError: true,
502
+ };
503
+ }
504
+ });
505
+ }
506
+ // Tool implementations
507
+ async getUserSession() {
508
+ const response = await this.axiosInstance.get('/user');
509
+ return {
510
+ content: [{
511
+ type: 'text',
512
+ text: JSON.stringify(response.data, null, 2),
513
+ }],
514
+ };
515
+ }
516
+ async getGlossary() {
517
+ if (Object.keys(this.glossary).length === 0) {
518
+ await this.initializeGlossary();
519
+ }
520
+ return {
521
+ content: [{
522
+ type: 'text',
523
+ text: JSON.stringify(this.glossary, null, 2),
524
+ }],
525
+ };
526
+ }
527
+ async getContentDefinition(args) {
528
+ const response = await this.axiosInstance.get(`/content/${args.type}/definition`);
529
+ return {
530
+ content: [{
531
+ type: 'text',
532
+ text: JSON.stringify(response.data, null, 2),
533
+ }],
534
+ };
535
+ }
536
+ async getContent(args) {
537
+ let response;
538
+ if (args.id) {
539
+ response = await this.axiosInstance.get(`/content/${args.id}`);
540
+ }
541
+ else if (args.slug) {
542
+ response = await this.axiosInstance.get(`/content/slug/${args.slug}`);
543
+ }
544
+ else {
545
+ throw new Error('Either id or slug must be provided');
546
+ }
547
+ return {
548
+ content: [{
549
+ type: 'text',
550
+ text: JSON.stringify(response.data, null, 2),
551
+ }],
552
+ };
553
+ }
554
+ async listContent(args) {
555
+ const response = await this.axiosInstance.post(`/content/${args.type}/list`, {
556
+ search: args.search || '',
557
+ filter: args.filter || {},
558
+ sort: args.sort || { key: 'meta.created', direction: 'desc', type: 'date' },
559
+ page: args.page || { size: 20, index: 1 },
560
+ select: args.select || [],
561
+ });
562
+ return {
563
+ content: [{
564
+ type: 'text',
565
+ text: JSON.stringify(response.data, null, 2),
566
+ }],
567
+ };
568
+ }
569
+ async createContent(args) {
570
+ const payload = {
571
+ title: args.title,
572
+ data: args.data || {},
573
+ meta: args.meta || {},
574
+ };
575
+ const response = await this.axiosInstance.post(`/content/${args.type}/create`, payload);
576
+ return {
577
+ content: [{
578
+ type: 'text',
579
+ text: JSON.stringify(response.data, null, 2),
580
+ }],
581
+ };
582
+ }
583
+ async updateContent(args) {
584
+ const method = args.replace ? 'put' : 'patch';
585
+ const response = await this.axiosInstance[method](`/content/${args.id}`, args.data);
586
+ return {
587
+ content: [{
588
+ type: 'text',
589
+ text: JSON.stringify(response.data, null, 2),
590
+ }],
591
+ };
592
+ }
593
+ async deleteContent(args) {
594
+ await this.axiosInstance.delete(`/content/${args.id}`);
595
+ return {
596
+ content: [{
597
+ type: 'text',
598
+ text: `Content ${args.id} deleted successfully`,
599
+ }],
600
+ };
601
+ }
602
+ async listProfiles(args) {
603
+ const response = await this.axiosInstance.post('/content/profile/list', {
604
+ search: args.search || '',
605
+ filter: args.filter || {},
606
+ page: args.page || { size: 20, index: 1 },
607
+ });
608
+ return {
609
+ content: [{
610
+ type: 'text',
611
+ text: JSON.stringify(response.data, null, 2),
612
+ }],
613
+ };
614
+ }
615
+ async createProfile(args) {
616
+ const payload = {
617
+ firstName: args.firstName,
618
+ lastName: args.lastName,
619
+ emails: args.emails || [],
620
+ phoneNumbers: args.phoneNumbers || [],
621
+ data: args.data || {},
622
+ meta: args.meta || {},
623
+ };
624
+ const response = await this.axiosInstance.post('/content/profile/create', payload);
625
+ return {
626
+ content: [{
627
+ type: 'text',
628
+ text: JSON.stringify(response.data, null, 2),
629
+ }],
630
+ };
631
+ }
632
+ async getForm(args) {
633
+ const response = await this.axiosInstance.get(`/form/${args.id}`);
634
+ return {
635
+ content: [{
636
+ type: 'text',
637
+ text: JSON.stringify(response.data, null, 2),
638
+ }],
639
+ };
640
+ }
641
+ async submitForm(args) {
642
+ const response = await this.axiosInstance.post(`/form/${args.id}`, args.data);
643
+ return {
644
+ content: [{
645
+ type: 'text',
646
+ text: JSON.stringify(response.data, null, 2),
647
+ }],
648
+ };
649
+ }
650
+ async uploadFile(args) {
651
+ const formData = new FormData();
652
+ // Convert base64 to buffer
653
+ const fileBuffer = Buffer.from(args.fileData, 'base64');
654
+ formData.append('file', fileBuffer, args.fileName);
655
+ const jsonData = {
656
+ title: args.title,
657
+ meta: args.meta || {},
658
+ };
659
+ formData.append('json', JSON.stringify(jsonData));
660
+ const response = await this.axiosInstance.post('/file/upload', formData, {
661
+ headers: {
662
+ ...formData.getHeaders(),
663
+ 'Authorization': `Bearer ${QIK_ACCESS_TOKEN}`,
664
+ },
665
+ });
666
+ return {
667
+ content: [{
668
+ type: 'text',
669
+ text: JSON.stringify(response.data, null, 2),
670
+ }],
671
+ };
672
+ }
673
+ async searchContent(args) {
674
+ const results = [];
675
+ const types = args.types || Object.keys(this.glossary);
676
+ for (const type of types.slice(0, 5)) { // Limit to 5 types to avoid too many requests
677
+ try {
678
+ const response = await this.axiosInstance.post(`/content/${type}/list`, {
679
+ search: args.query,
680
+ page: { size: args.limit || 20, index: 1 },
681
+ });
682
+ if (response.data.items && response.data.items.length > 0) {
683
+ results.push({
684
+ type,
685
+ items: response.data.items,
686
+ total: response.data.total,
687
+ });
688
+ }
689
+ }
690
+ catch (error) {
691
+ // Skip types that error (might not exist or no permission)
692
+ continue;
693
+ }
694
+ }
695
+ return {
696
+ content: [{
697
+ type: 'text',
698
+ text: JSON.stringify(results, null, 2),
699
+ }],
700
+ };
701
+ }
702
+ async getScopes() {
703
+ const response = await this.axiosInstance.get('/scope/tree');
704
+ return {
705
+ content: [{
706
+ type: 'text',
707
+ text: JSON.stringify(response.data, null, 2),
708
+ }],
709
+ };
710
+ }
711
+ async getSmartlist(args) {
712
+ const response = await this.axiosInstance.get(`/smartlist/${args.id}`);
713
+ return {
714
+ content: [{
715
+ type: 'text',
716
+ text: JSON.stringify(response.data, null, 2),
717
+ }],
718
+ };
719
+ }
720
+ async run() {
721
+ const transport = new StdioServerTransport();
722
+ await this.server.connect(transport);
723
+ console.error('Qik MCP server running on stdio');
724
+ }
725
+ }
726
+ // Only run the server if this file is executed directly (not imported)
727
+ if (import.meta.url === `file://${process.argv[1]}`) {
728
+ const server = new QikMCPServer();
729
+ server.run().catch(console.error);
730
+ }
731
+ //# sourceMappingURL=index.js.map