@qikdev/mcp 6.6.11 → 6.6.13

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,28 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Simplified Qik Platform MCP Server
3
+ * Qik Platform MCP Server
4
4
  *
5
- * A simple translation layer between AI models and the Qik API.
6
- * Focuses on core functionality with interactive error handling.
5
+ * MCP server for interacting with the Qik platform API.
6
+ * Provides tools for content management, user data, and more.
7
7
  */
8
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
10
  import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
11
- import axios from 'axios';
12
- import { ConfigManager } from './config.js';
13
- // Environment variables
14
- const QIK_API_URL = process.env.QIK_API_URL || 'https://api.qik.dev';
15
- const QIK_ACCESS_TOKEN = process.env.QIK_ACCESS_TOKEN;
11
+ import { tools, getToolHandler, hasToolHandler } from "./tools/index.js";
16
12
  export class QikMCPServer {
17
13
  server;
18
- axiosInstance;
19
- userSession = null;
20
- glossary = {};
21
- serverName = 'Qik';
22
14
  constructor() {
23
- if (!QIK_ACCESS_TOKEN) {
24
- throw new Error('QIK_ACCESS_TOKEN environment variable is required. Run "qik-mcp-server setup" to configure.');
25
- }
26
15
  this.server = new Server({
27
16
  name: "qik-mcp-server",
28
17
  version: "3.0.0",
@@ -31,609 +20,39 @@ export class QikMCPServer {
31
20
  tools: {},
32
21
  },
33
22
  });
34
- // Configure axios instance
35
- this.axiosInstance = axios.create({
36
- baseURL: QIK_API_URL,
37
- headers: {
38
- 'Authorization': `Bearer ${QIK_ACCESS_TOKEN}`,
39
- 'Content-Type': 'application/json',
40
- },
41
- timeout: 30000,
42
- });
43
23
  this.setupToolHandlers();
44
- this.initializeServer();
45
24
  // Error handling
46
- this.server.onerror = (error) => this.log(`MCP Error: ${error}`);
25
+ this.server.onerror = (error) => console.error(`MCP Error: ${error}`);
47
26
  process.on('SIGINT', async () => {
48
27
  await this.server.close();
49
28
  process.exit(0);
50
29
  });
51
30
  }
52
- log(message) {
53
- if (process.env.NODE_ENV !== 'production' || process.env.QIK_MCP_DEBUG === 'true') {
54
- process.stderr.write(`[Qik MCP] ${message}\n`);
55
- }
56
- }
57
- async initializeServer() {
58
- try {
59
- await this.loadServerName();
60
- await this.loadUserSession();
61
- await this.loadGlossary();
62
- this.log(`Initialized with ${Object.keys(this.glossary).length} content types`);
63
- }
64
- catch (error) {
65
- this.log(`Failed to initialize server: ${this.formatError(error)}`);
66
- }
67
- }
68
- async loadServerName() {
69
- try {
70
- const configManager = new ConfigManager();
71
- const config = await configManager.loadConfig();
72
- if (config && config.serverName) {
73
- this.serverName = config.serverName;
74
- }
75
- }
76
- catch (error) {
77
- this.log(`Failed to load server name: ${this.formatError(error)}`);
78
- }
79
- }
80
- async loadUserSession() {
81
- try {
82
- const response = await this.axiosInstance.get('/user');
83
- this.userSession = response.data.session || response.data;
84
- this.log(`Authenticated as ${this.userSession?.firstName} ${this.userSession?.lastName}`);
85
- }
86
- catch (error) {
87
- this.log(`Failed to load user session: ${this.formatError(error)}`);
88
- }
89
- }
90
- async loadGlossary() {
91
- try {
92
- const response = await this.axiosInstance.get('/glossary/ai');
93
- this.glossary = (response.data || []).reduce((memo, definition) => {
94
- if (definition.key) {
95
- memo[definition.key] = definition;
96
- }
97
- return memo;
98
- }, {});
99
- this.log(`Loaded ${Object.keys(this.glossary).length} content types`);
100
- }
101
- catch (error) {
102
- this.log(`Failed to load glossary: ${this.formatError(error)}`);
103
- }
104
- }
105
- formatError(error) {
106
- if (axios.isAxiosError(error)) {
107
- const axiosError = error;
108
- if (axiosError.response) {
109
- const status = axiosError.response.status;
110
- const data = axiosError.response.data;
111
- return `HTTP ${status}: ${JSON.stringify(data)}`;
112
- }
113
- return `Network error: ${axiosError.message}`;
114
- }
115
- return error.message || String(error);
116
- }
117
- async promptUserForScopes() {
118
- try {
119
- const response = await this.axiosInstance.get('/scope/tree');
120
- const scopes = this.extractAvailableScopes(response.data);
121
- if (scopes.length === 0) {
122
- throw new Error('No scopes available for content creation');
123
- }
124
- return {
125
- content: [{
126
- type: 'text',
127
- text: `🔐 **SCOPE SELECTION REQUIRED**
128
-
129
- To create content, you need to specify which scope(s) to create it in.
130
-
131
- **Available Scopes:**
132
- ${scopes.map(s => `- **${s.id}**: ${s.title} (${s.path})`).join('\n')}
133
-
134
- Please retry your request with the scope ID(s) you want to use in the meta.scopes field.
135
-
136
- **Example:**
137
- \`\`\`json
138
- {
139
- "meta": {
140
- "scopes": ["${scopes[0].id}"]
141
- }
142
- }
143
- \`\`\``,
144
- }],
145
- isError: true,
146
- };
147
- }
148
- catch (error) {
149
- throw new Error(`Failed to get available scopes: ${this.formatError(error)}`);
150
- }
151
- }
152
- extractAvailableScopes(scopeTree) {
153
- const scopes = [];
154
- const traverse = (node, path = '') => {
155
- if (!node)
156
- return;
157
- const currentPath = path ? `${path} > ${node.title || node.name || node._id}` : (node.title || node.name || node._id);
158
- if (node.permissions && node.permissions.create) {
159
- scopes.push({
160
- id: node._id,
161
- title: node.title || node.name || node._id,
162
- path: currentPath
163
- });
164
- }
165
- if (node.children && Array.isArray(node.children)) {
166
- for (const child of node.children) {
167
- traverse(child, currentPath);
168
- }
169
- }
170
- };
171
- if (Array.isArray(scopeTree)) {
172
- for (const scope of scopeTree) {
173
- traverse(scope);
174
- }
175
- }
176
- else {
177
- traverse(scopeTree);
178
- }
179
- return scopes;
180
- }
181
- async handleApiError(error, operation, args) {
182
- if (error.response?.status === 400) {
183
- const errorData = error.response.data;
184
- // Handle missing scopes
185
- if (errorData.message && errorData.message.includes('scope')) {
186
- return await this.promptUserForScopes();
187
- }
188
- // Handle validation errors - let user know what went wrong
189
- return {
190
- content: [{
191
- type: 'text',
192
- text: `❌ **VALIDATION ERROR**
193
-
194
- The API rejected your request for ${operation}:
195
-
196
- **Error Details:**
197
- ${JSON.stringify(errorData, null, 2)}
198
-
199
- **Your Request:**
200
- ${JSON.stringify(args, null, 2)}
201
-
202
- **Suggestions:**
203
- - Check that all required fields are provided
204
- - Verify field names match the content type definition
205
- - Ensure scope IDs are valid and you have permissions
206
- - Use \`qik_get_content_definition\` to see the exact field structure`,
207
- }],
208
- isError: true,
209
- };
210
- }
211
- if (error.response?.status === 403) {
212
- return {
213
- content: [{
214
- type: 'text',
215
- text: `🚫 **PERMISSION DENIED**
216
-
217
- You don't have permission to perform ${operation}.
218
-
219
- **Possible causes:**
220
- - Your access token doesn't have the required permissions
221
- - The scope you're trying to access is restricted
222
- - The content type requires special permissions
223
-
224
- **Next steps:**
225
- - Check your token permissions with your administrator
226
- - Try using \`qik_get_scopes\` to see available scopes
227
- - Verify you have create/update permissions for this content type`,
228
- }],
229
- isError: true,
230
- };
231
- }
232
- // Generic error handling
233
- return {
234
- content: [{
235
- type: 'text',
236
- text: `❌ **API ERROR**
237
-
238
- Failed to ${operation}:
239
-
240
- **Error:** ${this.formatError(error)}
241
-
242
- **Your Request:**
243
- ${JSON.stringify(args, null, 2)}
244
-
245
- **Troubleshooting:**
246
- - Check your internet connection
247
- - Verify your access token is valid
248
- - Try the request again in a few moments`,
249
- }],
250
- isError: true,
251
- };
252
- }
253
31
  setupToolHandlers() {
32
+ // List available tools
254
33
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
255
- const tools = [
256
- // Core requirement 1: Authentication
257
- {
258
- name: 'qik_get_user_session',
259
- description: '👤 Get current user session information',
260
- inputSchema: {
261
- type: 'object',
262
- properties: {},
263
- },
264
- },
265
- // Core requirement 2: Glossary discovery
266
- {
267
- name: 'qik_get_glossary',
268
- description: '📚 Get all available content types and their definitions',
269
- inputSchema: {
270
- type: 'object',
271
- properties: {},
272
- },
273
- },
274
- {
275
- name: 'qik_get_content_definition',
276
- description: '🔍 Get definition for a specific content type',
277
- inputSchema: {
278
- type: 'object',
279
- properties: {
280
- type: {
281
- type: 'string',
282
- description: 'Content type key',
283
- enum: Object.keys(this.glossary),
284
- },
285
- },
286
- required: ['type'],
287
- },
288
- },
289
- // Core requirement 3: Session management (covered by user session)
290
- {
291
- name: 'qik_get_scopes',
292
- description: '🔐 Get available scopes/permissions tree',
293
- inputSchema: {
294
- type: 'object',
295
- properties: {},
296
- },
297
- },
298
- // Core requirement 4: Basic CRUD tools
299
- {
300
- name: 'qik_get_content',
301
- description: '📄 Get content item by ID or slug',
302
- inputSchema: {
303
- type: 'object',
304
- properties: {
305
- id: { type: 'string', description: 'Content ID' },
306
- slug: { type: 'string', description: 'Content slug' },
307
- },
308
- oneOf: [
309
- { required: ['id'] },
310
- { required: ['slug'] },
311
- ],
312
- },
313
- },
314
- {
315
- name: 'qik_list_content',
316
- description: '📋 List content items with filtering and search',
317
- inputSchema: {
318
- type: 'object',
319
- properties: {
320
- type: {
321
- type: 'string',
322
- description: 'Content type to list',
323
- enum: Object.keys(this.glossary),
324
- },
325
- search: { type: 'string', description: 'Search keywords' },
326
- filter: { type: 'object', description: 'Filter criteria' },
327
- page: {
328
- type: 'object',
329
- properties: {
330
- size: { type: 'number', minimum: 1, maximum: 100 },
331
- index: { type: 'number', minimum: 1 },
332
- },
333
- },
334
- },
335
- required: ['type'],
336
- },
337
- },
338
- {
339
- name: 'qik_create_content',
340
- description: '✨ Create new content item',
341
- inputSchema: {
342
- type: 'object',
343
- properties: {
344
- type: {
345
- type: 'string',
346
- description: 'Content type to create',
347
- enum: Object.keys(this.glossary),
348
- },
349
- title: { type: 'string', description: 'Content title' },
350
- data: { type: 'object', description: 'Content data fields' },
351
- meta: {
352
- type: 'object',
353
- description: 'Meta information (scopes required)',
354
- properties: {
355
- scopes: {
356
- type: 'array',
357
- items: { type: 'string' },
358
- description: 'Scope IDs where content should be created',
359
- },
360
- tags: { type: 'array', items: { type: 'string' } },
361
- security: { type: 'string', enum: ['public', 'secure', 'private'] },
362
- },
363
- },
364
- },
365
- required: ['type', 'title'],
366
- },
367
- },
368
- {
369
- name: 'qik_update_content',
370
- description: '✏️ Update existing content item',
371
- inputSchema: {
372
- type: 'object',
373
- properties: {
374
- id: { type: 'string', description: 'Content ID to update' },
375
- data: { type: 'object', description: 'Data to update' },
376
- },
377
- required: ['id', 'data'],
378
- },
379
- },
380
- {
381
- name: 'qik_delete_content',
382
- description: '🗑️ Delete content item',
383
- inputSchema: {
384
- type: 'object',
385
- properties: {
386
- id: { type: 'string', description: 'Content ID to delete' },
387
- },
388
- required: ['id'],
389
- },
390
- },
391
- ];
392
34
  return { tools };
393
35
  });
36
+ // Handle tool calls
394
37
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
38
+ const { name, arguments: args } = request.params;
39
+ if (!hasToolHandler(name)) {
40
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
41
+ }
395
42
  try {
396
- switch (request.params.name) {
397
- case 'qik_get_user_session':
398
- return await this.getUserSession();
399
- case 'qik_get_glossary':
400
- return await this.getGlossary();
401
- case 'qik_get_content_definition':
402
- return await this.getContentDefinition(request.params.arguments);
403
- case 'qik_get_scopes':
404
- return await this.getScopes();
405
- case 'qik_get_content':
406
- return await this.getContent(request.params.arguments);
407
- case 'qik_list_content':
408
- return await this.listContent(request.params.arguments);
409
- case 'qik_create_content':
410
- return await this.createContent(request.params.arguments);
411
- case 'qik_update_content':
412
- return await this.updateContent(request.params.arguments);
413
- case 'qik_delete_content':
414
- return await this.deleteContent(request.params.arguments);
415
- default:
416
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
417
- }
43
+ const handler = getToolHandler(name);
44
+ const result = await handler(args);
45
+ return result;
418
46
  }
419
47
  catch (error) {
420
- this.log(`Error in ${request.params.name}: ${this.formatError(error)}`);
421
- if (axios.isAxiosError(error)) {
422
- return await this.handleApiError(error, request.params.name, request.params.arguments);
423
- }
424
- return {
425
- content: [{
426
- type: 'text',
427
- text: `Error: ${this.formatError(error)}`,
428
- }],
429
- isError: true,
430
- };
48
+ throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error.message}`);
431
49
  }
432
50
  });
433
51
  }
434
- // Simplified tool implementations - let API handle validation
435
- async getUserSession() {
436
- if (!this.userSession) {
437
- await this.loadUserSession();
438
- }
439
- return {
440
- content: [{
441
- type: 'text',
442
- text: JSON.stringify(this.userSession, null, 2),
443
- }],
444
- };
445
- }
446
- async getGlossary() {
447
- await this.loadGlossary();
448
- const contentTypes = Object.entries(this.glossary).map(([key, type]) => ({
449
- key,
450
- title: type.title,
451
- plural: type.plural,
452
- description: type.description,
453
- fieldCount: type.fields?.length || 0,
454
- }));
455
- return {
456
- content: [{
457
- type: 'text',
458
- text: `Available Content Types (${contentTypes.length} total):
459
-
460
- ${contentTypes.map(t => `${t.key}: ${t.title} (${t.fieldCount} fields)`).join('\n')}
461
-
462
- Full glossary data:
463
- ${JSON.stringify(this.glossary, null, 2)}`,
464
- }],
465
- };
466
- }
467
- async getContentDefinition(args) {
468
- if (!this.glossary[args.type]) {
469
- const available = Object.keys(this.glossary).join(', ');
470
- return {
471
- content: [{
472
- type: 'text',
473
- text: `Content type '${args.type}' not found. Available types: ${available}`,
474
- }],
475
- isError: true,
476
- };
477
- }
478
- try {
479
- const response = await this.axiosInstance.get(`/content/${args.type}/definition`);
480
- return {
481
- content: [{
482
- type: 'text',
483
- text: JSON.stringify(response.data, null, 2),
484
- }],
485
- };
486
- }
487
- catch (error) {
488
- if (axios.isAxiosError(error)) {
489
- return await this.handleApiError(error, 'get content definition', args);
490
- }
491
- throw error;
492
- }
493
- }
494
- async getScopes() {
495
- try {
496
- const response = await this.axiosInstance.get('/scope/tree');
497
- return {
498
- content: [{
499
- type: 'text',
500
- text: JSON.stringify(response.data, null, 2),
501
- }],
502
- };
503
- }
504
- catch (error) {
505
- if (axios.isAxiosError(error)) {
506
- return await this.handleApiError(error, 'get scopes', {});
507
- }
508
- throw error;
509
- }
510
- }
511
- async getContent(args) {
512
- try {
513
- let response;
514
- if (args.id) {
515
- response = await this.axiosInstance.get(`/content/${args.id}`);
516
- }
517
- else if (args.slug) {
518
- response = await this.axiosInstance.get(`/content/slug/${args.slug}`);
519
- }
520
- else {
521
- throw new Error('Either id or slug must be provided');
522
- }
523
- return {
524
- content: [{
525
- type: 'text',
526
- text: JSON.stringify(response.data, null, 2),
527
- }],
528
- };
529
- }
530
- catch (error) {
531
- if (axios.isAxiosError(error)) {
532
- return await this.handleApiError(error, 'get content', args);
533
- }
534
- throw error;
535
- }
536
- }
537
- async listContent(args) {
538
- if (!this.glossary[args.type]) {
539
- const available = Object.keys(this.glossary).join(', ');
540
- return {
541
- content: [{
542
- type: 'text',
543
- text: `Content type '${args.type}' not found. Available types: ${available}`,
544
- }],
545
- isError: true,
546
- };
547
- }
548
- try {
549
- const response = await this.axiosInstance.post(`/content/${args.type}/list`, {
550
- search: args.search || '',
551
- filter: args.filter || {},
552
- page: args.page || { size: 20, index: 1 },
553
- });
554
- return {
555
- content: [{
556
- type: 'text',
557
- text: JSON.stringify(response.data, null, 2),
558
- }],
559
- };
560
- }
561
- catch (error) {
562
- if (axios.isAxiosError(error)) {
563
- return await this.handleApiError(error, 'list content', args);
564
- }
565
- throw error;
566
- }
567
- }
568
- async createContent(args) {
569
- if (!this.glossary[args.type]) {
570
- const available = Object.keys(this.glossary).join(', ');
571
- return {
572
- content: [{
573
- type: 'text',
574
- text: `Content type '${args.type}' not found. Available types: ${available}`,
575
- }],
576
- isError: true,
577
- };
578
- }
579
- try {
580
- const response = await this.axiosInstance.post(`/content/${args.type}/create`, args);
581
- return {
582
- content: [{
583
- type: 'text',
584
- text: `✅ Successfully created ${args.type}:
585
-
586
- ${JSON.stringify(response.data, null, 2)}`,
587
- }],
588
- };
589
- }
590
- catch (error) {
591
- if (axios.isAxiosError(error)) {
592
- return await this.handleApiError(error, 'create content', args);
593
- }
594
- throw error;
595
- }
596
- }
597
- async updateContent(args) {
598
- try {
599
- const response = await this.axiosInstance.patch(`/content/${args.id}`, args.data);
600
- return {
601
- content: [{
602
- type: 'text',
603
- text: `✅ Successfully updated content:
604
-
605
- ${JSON.stringify(response.data, null, 2)}`,
606
- }],
607
- };
608
- }
609
- catch (error) {
610
- if (axios.isAxiosError(error)) {
611
- return await this.handleApiError(error, 'update content', args);
612
- }
613
- throw error;
614
- }
615
- }
616
- async deleteContent(args) {
617
- try {
618
- await this.axiosInstance.delete(`/content/${args.id}`);
619
- return {
620
- content: [{
621
- type: 'text',
622
- text: `✅ Successfully deleted content ${args.id}`,
623
- }],
624
- };
625
- }
626
- catch (error) {
627
- if (axios.isAxiosError(error)) {
628
- return await this.handleApiError(error, 'delete content', args);
629
- }
630
- throw error;
631
- }
632
- }
633
52
  async run() {
634
53
  const transport = new StdioServerTransport();
635
54
  await this.server.connect(transport);
636
- this.log('Simplified Qik MCP server running on stdio');
55
+ console.error('Qik MCP server running on stdio');
637
56
  }
638
57
  }
639
58
  // Only run the server if this file is executed directly