@testomatio/mcp 1.0.14 → 2.0.0-beta.8

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.
package/index.js CHANGED
@@ -1,1623 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema,
8
- } from '@modelcontextprotocol/sdk/types.js';
9
- import { program } from 'commander';
10
3
  import { fileURLToPath } from 'url';
11
-
12
- function normalizeString(value) {
13
- return typeof value === 'string' ? value.trim() : value;
14
- }
15
-
16
- function normalizeBaseUrl(value) {
17
- if (typeof value !== 'string') {
18
- return value;
19
- }
20
-
21
- const trimmed = value.trim();
22
- // Remove any internal whitespace characters that may appear when the URL
23
- // gets broken across lines (e.g. "http://\n localhost:3000").
24
- return trimmed.replace(/\s+/g, '');
25
- }
26
-
27
- class TestomatioMCPServer {
28
- constructor(config) {
29
- this.config = {
30
- ...config,
31
- token: normalizeString(config.token),
32
- projectId: normalizeString(config.projectId),
33
- baseUrl: normalizeBaseUrl(config.baseUrl),
34
- };
35
- this.jwtToken = null;
36
- this.server = new Server(
37
- {
38
- name: 'testomatio-mcp-server',
39
- version: '1.0.0',
40
- },
41
- {
42
- capabilities: {
43
- tools: {},
44
- },
45
- }
46
- );
47
-
48
- this.setupToolHandlers();
49
- }
50
-
51
- async authenticate() {
52
- if (this.jwtToken) {
53
- return this.jwtToken;
54
- }
55
-
56
- const response = await fetch(`${this.config.baseUrl}/api/login`, {
57
- method: 'POST',
58
- headers: {
59
- 'Content-Type': 'application/x-www-form-urlencoded',
60
- },
61
- body: `api_token=${this.config.token}`,
62
- });
63
-
64
- if (!response.ok) {
65
- const errorText = await response.text();
66
- throw new Error(`Authentication failed: HTTP ${response.status}: ${response.statusText}. Response: ${errorText}`);
67
- }
68
-
69
- const data = await response.json();
70
-
71
- if (!data.jwt) {
72
- throw new Error('Authentication failed: No JWT token received in response');
73
- }
74
-
75
- this.jwtToken = data.jwt;
76
-
77
- return this.jwtToken;
78
- }
79
-
80
- setupToolHandlers() {
81
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
82
- return {
83
- tools: [
84
- {
85
- name: 'get_tests',
86
- description: 'Get all tests for the project with optional filtering',
87
- inputSchema: {
88
- type: 'object',
89
- properties: {
90
- plan: {
91
- type: 'string',
92
- description: 'Plan ID to fetch tests from specific plan',
93
- },
94
- query: {
95
- type: 'string',
96
- description: 'Search by text or query language (start with =). Example: "=tag == \'slow\'"',
97
- },
98
- state: {
99
- type: 'string',
100
- enum: ['manual', 'automated'],
101
- description: 'Filter by test state',
102
- },
103
- suite_id: {
104
- type: 'string',
105
- description: 'Get tests from specific suite',
106
- },
107
- tag: {
108
- type: 'string',
109
- description: 'Filter by tag (e.g., @slow)',
110
- },
111
- labels: {
112
- type: 'array',
113
- items: { type: 'string' },
114
- description: 'Filter by labels array',
115
- },
116
- },
117
- },
118
- },
119
- {
120
- name: 'get_test',
121
- description: 'Get a specific test by its ID with all information including labels, tags, and metadata',
122
- inputSchema: {
123
- type: 'object',
124
- properties: {
125
- test_id: {
126
- type: 'string',
127
- description: 'The ID of the test to retrieve',
128
- },
129
- },
130
- required: ['test_id'],
131
- },
132
- },
133
- {
134
- name: 'search_tests',
135
- description: 'Search tests by keywords, tags, labels, TQL queries, and other filters',
136
- inputSchema: {
137
- type: 'object',
138
- properties: {
139
- query: {
140
- type: 'string',
141
- description: 'Search by keywords, tags (@smoke), or Jira issues (JIRA-123)',
142
- },
143
- tql: {
144
- type: 'string',
145
- description: 'Test Query Language for advanced filtering (e.g., "tag == \'smoke\' and state == \'manual\'")',
146
- },
147
- labels: {
148
- type: 'array',
149
- items: { type: 'string' },
150
- description: 'Filter by labels (e.g., ["ui", "critical"])',
151
- },
152
- state: {
153
- type: 'string',
154
- enum: ['manual', 'automated'],
155
- description: 'Filter by test state',
156
- },
157
- priority: {
158
- type: 'string',
159
- enum: ['low', 'normal', 'high', 'critical'],
160
- description: 'Filter by priority level',
161
- },
162
- filter: {
163
- type: 'object',
164
- description: 'Advanced filter hash (e.g., {state: "manual", priority: "high"})',
165
- additionalProperties: true,
166
- },
167
- page: {
168
- type: 'number',
169
- description: 'Page number for pagination',
170
- },
171
- },
172
- },
173
- },
174
- {
175
- name: 'search_suites',
176
- description: 'Search suites and their tests by keywords, tags, labels, and other filters',
177
- inputSchema: {
178
- type: 'object',
179
- properties: {
180
- query: {
181
- type: 'string',
182
- description: 'Search by keywords, tags (@smoke), or Jira issues (JIRA-123)',
183
- },
184
- labels: {
185
- type: 'array',
186
- items: { type: 'string' },
187
- description: 'Filter by labels (e.g., ["ui", "critical"])',
188
- },
189
- state: {
190
- type: 'string',
191
- enum: ['manual', 'automated'],
192
- description: 'Filter by test state',
193
- },
194
- priority: {
195
- type: 'string',
196
- enum: ['low', 'normal', 'high', 'critical'],
197
- description: 'Filter by priority level',
198
- },
199
- page: {
200
- type: 'number',
201
- description: 'Page number for pagination',
202
- },
203
- },
204
- },
205
- },
206
- {
207
- name: 'get_root_suites',
208
- description: 'Get all root-level suites for the project',
209
- inputSchema: {
210
- type: 'object',
211
- properties: {},
212
- },
213
- },
214
- {
215
- name: 'get_suite',
216
- description: 'Get a specific suite with its child suites and tests',
217
- inputSchema: {
218
- type: 'object',
219
- properties: {
220
- suite_id: {
221
- type: 'string',
222
- description: 'Suite identifier',
223
- },
224
- },
225
- required: ['suite_id'],
226
- },
227
- },
228
- {
229
- name: 'get_runs',
230
- description: 'Get all test runs for the project',
231
- inputSchema: {
232
- type: 'object',
233
- properties: {},
234
- },
235
- },
236
- {
237
- name: 'get_run',
238
- description: 'Get a specific test run with detailed information',
239
- inputSchema: {
240
- type: 'object',
241
- properties: {
242
- run_id: {
243
- type: 'string',
244
- description: 'Run identifier',
245
- },
246
- tree: {
247
- type: 'boolean',
248
- description: 'Include list of tests',
249
- },
250
- },
251
- required: ['run_id'],
252
- },
253
- },
254
- {
255
- name: 'get_testruns',
256
- description: 'Get test runs for a specific test with optional date filtering',
257
- inputSchema: {
258
- type: 'object',
259
- properties: {
260
- test_id: {
261
- type: 'string',
262
- description: 'Test identifier',
263
- },
264
- finished_at_date_range: {
265
- type: 'string',
266
- description: 'Date range filter (format: YYYY-MM-DD,YYYY-MM-DD)',
267
- },
268
- },
269
- required: ['test_id'],
270
- },
271
- },
272
- {
273
- name: 'get_plans',
274
- description: 'Get all test plans for the project',
275
- inputSchema: {
276
- type: 'object',
277
- properties: {
278
- detail: {
279
- type: 'boolean',
280
- description: 'Include detailed information',
281
- },
282
- labels: {
283
- type: 'array',
284
- items: { type: 'string' },
285
- description: 'Filter by labels array',
286
- },
287
- page: {
288
- type: 'number',
289
- description: 'Page number for pagination',
290
- },
291
- },
292
- },
293
- },
294
- {
295
- name: 'get_plan',
296
- description: 'Get a specific test plan with attached items',
297
- inputSchema: {
298
- type: 'object',
299
- properties: {
300
- plan_id: {
301
- type: 'string',
302
- description: 'Plan identifier',
303
- },
304
- },
305
- required: ['plan_id'],
306
- },
307
- },
308
- {
309
- name: 'create_test',
310
- description: 'Create a new test in the specified suite',
311
- inputSchema: {
312
- type: 'object',
313
- properties: {
314
- suite_id: {
315
- type: 'string',
316
- description: 'Suite ID where the test will be created',
317
- },
318
- title: {
319
- type: 'string',
320
- description: 'Test title. @tags in the title (e.g., "@smoke test") will be automatically extracted as tags',
321
- },
322
- description: {
323
- type: 'string',
324
- description: 'Test description',
325
- },
326
- code: {
327
- type: 'string',
328
- description: 'Source code of an automated test',
329
- },
330
- file: {
331
- type: 'string',
332
- description: 'File of an automated test',
333
- },
334
- state: {
335
- type: 'string',
336
- enum: ['manual', 'automated'],
337
- description: 'State of the test',
338
- },
339
- tags: {
340
- type: 'array',
341
- items: { type: 'string' },
342
- description: 'List of @tags for the test. Tags are automatically extracted from @ mentions in the title (e.g., @smoke, @regression). Can also be provided as an array of tag names (without @ prefix).',
343
- },
344
- jira_issues: {
345
- type: 'array',
346
- items: { type: 'string' },
347
- description: 'List of assigned Jira issues',
348
- },
349
- assigned_to: {
350
- type: 'string',
351
- description: 'User assigned to this test',
352
- },
353
- labels_ids: {
354
- type: 'array',
355
- items: { type: 'string' },
356
- description: 'Slugs of labels to assign to the test. Supports label:value format (e.g., ["priority:high", "severity:critical"])',
357
- },
358
- fields: {
359
- type: 'object',
360
- description: 'Set custom fields for the test. Object with field names as keys and values as properties (e.g., {"priority": "high", "severity": "critical"})',
361
- additionalProperties: {
362
- type: 'string'
363
- },
364
- },
365
- },
366
- required: ['suite_id', 'title'],
367
- },
368
- },
369
- {
370
- name: 'update_test',
371
- description: 'Update an existing test',
372
- inputSchema: {
373
- type: 'object',
374
- properties: {
375
- test_id: {
376
- type: 'string',
377
- description: 'ID of the test to update',
378
- },
379
- suite_id: {
380
- type: 'string',
381
- description: 'Suite ID where the test belongs',
382
- },
383
- title: {
384
- type: 'string',
385
- description: 'Test title. @tags in the title (e.g., "@smoke test") will be automatically extracted as tags',
386
- },
387
- description: {
388
- type: 'string',
389
- description: 'Test description',
390
- },
391
- code: {
392
- type: 'string',
393
- description: 'Source code of an automated test',
394
- },
395
- file: {
396
- type: 'string',
397
- description: 'File of an automated test',
398
- },
399
- state: {
400
- type: 'string',
401
- enum: ['manual', 'automated'],
402
- description: 'State of the test',
403
- },
404
- priority: {
405
- type: 'string',
406
- enum: ['low', 'normal', 'high', 'critical'],
407
- description: 'Priority level of the test',
408
- },
409
- tags: {
410
- type: 'array',
411
- items: { type: 'string' },
412
- description: 'List of @tags for the test. Tags are automatically extracted from @ mentions in the title (e.g., @smoke, @regression). Can also be provided as an array of tag names (without @ prefix).',
413
- },
414
- jira_issues: {
415
- type: 'array',
416
- items: { type: 'string' },
417
- description: 'List of assigned Jira issues',
418
- },
419
- assigned_to: {
420
- type: 'string',
421
- description: 'User assigned to this test',
422
- },
423
- labels_ids: {
424
- type: 'array',
425
- items: { type: 'string' },
426
- description: 'Slugs of labels to assign to the test. Supports label:value format (e.g., ["priority:high", "severity:critical"])',
427
- },
428
- fields: {
429
- type: 'object',
430
- description: 'Set custom fields for the test. Object with field names as keys and values as properties (e.g., {"priority": "high", "severity": "critical"})',
431
- additionalProperties: {
432
- type: 'string'
433
- },
434
- },
435
- },
436
- required: ['test_id'],
437
- },
438
- },
439
- {
440
- name: 'create_suite',
441
- description: 'Create a new suite. Suites can only contain other suites (no tests or folders)',
442
- inputSchema: {
443
- type: 'object',
444
- properties: {
445
- title: {
446
- type: 'string',
447
- description: 'Suite title',
448
- },
449
- description: {
450
- type: 'string',
451
- description: 'Suite description',
452
- },
453
- parent_id: {
454
- type: 'string',
455
- description: 'Parent suite ID to create this suite under',
456
- },
457
- fields: {
458
- type: 'object',
459
- description: 'Set custom fields for the suite. Object with field names as keys and values as properties (e.g., {"priority": "high", "team": "backend"})',
460
- additionalProperties: {
461
- type: 'string'
462
- },
463
- },
464
- },
465
- required: ['title'],
466
- },
467
- },
468
- {
469
- name: 'create_folder',
470
- description: 'Create a new folder. Folders can contain suites and folders (but no tests)',
471
- inputSchema: {
472
- type: 'object',
473
- properties: {
474
- title: {
475
- type: 'string',
476
- description: 'Folder title',
477
- },
478
- description: {
479
- type: 'string',
480
- description: 'Folder description',
481
- },
482
- parent_id: {
483
- type: 'string',
484
- description: 'Parent folder or suite ID to create this folder under',
485
- },
486
- fields: {
487
- type: 'object',
488
- description: 'Set custom fields for the folder. Object with field names as keys and values as properties (e.g., {"priority": "high", "team": "backend"})',
489
- additionalProperties: {
490
- type: 'string'
491
- },
492
- },
493
- },
494
- required: ['title'],
495
- },
496
- },
497
- {
498
- name: 'get_labels',
499
- description: 'Get all available labels for the project with their IDs and configurations',
500
- inputSchema: {
501
- type: 'object',
502
- properties: {
503
- scope: {
504
- type: 'array',
505
- items: {
506
- type: 'string',
507
- enum: ['tests', 'suites']
508
- },
509
- description: 'Filter labels by scope (e.g., ["tests"], ["suites"], or ["tests", "suites"])',
510
- },
511
- page: {
512
- type: 'number',
513
- description: 'Page number for pagination',
514
- },
515
- },
516
- },
517
- },
518
- {
519
- name: 'unlink_label',
520
- description: 'Remove a label from a test or suite. Can remove specific label values or all instances of the label',
521
- inputSchema: {
522
- type: 'object',
523
- properties: {
524
- label_id: {
525
- type: 'string',
526
- description: 'Label ID to remove (e.g., "priority", "severity")',
527
- },
528
- test_id: {
529
- type: 'string',
530
- description: 'Test ID to remove label from (use either test_id or suite_id)',
531
- },
532
- suite_id: {
533
- type: 'string',
534
- description: 'Suite ID to remove label from (use either test_id or suite_id)',
535
- },
536
- value: {
537
- type: 'string',
538
- description: 'Specific label value to remove (e.g., "high", "critical"). If omitted, all instances are removed',
539
- },
540
- },
541
- required: ['label_id'],
542
- },
543
- },
544
- {
545
- name: 'create_label',
546
- description: 'Create a new label with optional custom field configuration. Labels can be used to tag and categorize tests and suites',
547
- inputSchema: {
548
- type: 'object',
549
- properties: {
550
- title: {
551
- type: 'string',
552
- description: 'Label title (e.g., "Severity", "Priority", "Type")',
553
- },
554
- color: {
555
- type: 'string',
556
- description: 'Label color in hex format (e.g., "#ffe9ad")',
557
- },
558
- scope: {
559
- type: 'array',
560
- items: {
561
- type: 'string',
562
- enum: ['tests', 'suites']
563
- },
564
- description: 'Where this label can be used (e.g., ["tests", "suites"])',
565
- },
566
- visibility: {
567
- type: 'array',
568
- items: {
569
- type: 'string'
570
- },
571
- description: 'Where the label is visible (e.g., ["list"])',
572
- },
573
- field: {
574
- type: 'object',
575
- description: 'Custom field configuration for labels with predefined values',
576
- properties: {
577
- type: {
578
- type: 'string',
579
- description: 'Field type (e.g., "list", "string", "number")',
580
- },
581
- short: {
582
- type: 'boolean',
583
- description: 'Whether to display short version',
584
- },
585
- value: {
586
- type: 'string',
587
- description: 'Predefined values for the field (newline separated for list type)',
588
- },
589
- },
590
- required: ['type'],
591
- },
592
- },
593
- required: ['title'],
594
- },
595
- },
596
- ],
597
- };
598
- });
599
-
600
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
601
- const { name, arguments: args } = request.params;
602
-
603
- try {
604
- switch (name) {
605
- case 'get_tests':
606
- return await this.getTests(args);
607
- case 'get_test':
608
- return await this.getTest(args.test_id);
609
- case 'search_tests':
610
- return await this.searchTests(args);
611
- case 'search_suites':
612
- return await this.searchSuites(args);
613
- case 'get_root_suites':
614
- return await this.getRootSuites();
615
- case 'get_suite':
616
- return await this.getSuite(args.suite_id);
617
- case 'get_runs':
618
- return await this.getRuns();
619
- case 'get_run':
620
- return await this.getRun(args.run_id, args.tree);
621
- case 'get_testruns':
622
- return await this.getTestruns(args.test_id, args.finished_at_date_range);
623
- case 'get_plans':
624
- return await this.getPlans(args);
625
- case 'get_plan':
626
- return await this.getPlan(args.plan_id);
627
- case 'get_labels':
628
- return await this.getLabels(args);
629
- case 'unlink_label':
630
- return await this.unlinkLabel(args);
631
- case 'create_test':
632
- return await this.createTest(args);
633
- case 'update_test':
634
- return await this.updateTest(args);
635
- case 'create_suite':
636
- return await this.createSuite(args);
637
- case 'create_folder':
638
- return await this.createFolder(args);
639
- case 'create_label':
640
- return await this.createLabel(args);
641
- default:
642
- throw new Error(`Unknown tool: ${name}`);
643
- }
644
- } catch (error) {
645
- return {
646
- content: [
647
- {
648
- type: 'text',
649
- text: `Error: ${error.message}`,
650
- },
651
- ],
652
- };
653
- }
654
- });
655
- }
656
-
657
- async makeRequest(path, params = {}) {
658
- // Ensure we have a valid JWT token
659
- const jwt = await this.authenticate();
660
-
661
- const url = new URL(`${this.config.baseUrl}/api/${this.config.projectId}${path}`);
662
-
663
- // Add query parameters with proper array handling
664
- Object.entries(params).forEach(([key, value]) => {
665
- if (value !== undefined && value !== null) {
666
- if (Array.isArray(value)) {
667
- // Handle arrays (e.g., labels[])
668
- value.forEach(v => url.searchParams.append(key, v));
669
- } else {
670
- url.searchParams.append(key, String(value));
671
- }
672
- }
673
- });
674
-
675
- const response = await fetch(url.toString(), {
676
- method: 'GET',
677
- headers: {
678
- 'Authorization': jwt,
679
- 'Content-Type': 'application/json',
680
- },
681
- });
682
-
683
- if (!response.ok) {
684
- // If unauthorized, clear the JWT token and retry once
685
- if (response.status === 401 && this.jwtToken) {
686
- this.jwtToken = null;
687
- return this.makeRequest(path, params);
688
- }
689
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
690
- }
691
-
692
- return await response.json();
693
- }
694
-
695
- async makePostRequest(path, data) {
696
- const jwt = await this.authenticate();
697
- const url = `${this.config.baseUrl}/api/${this.config.projectId}${path}`;
698
-
699
- const response = await fetch(url, {
700
- method: 'POST',
701
- headers: {
702
- 'Authorization': jwt,
703
- 'Content-Type': 'application/json',
704
- },
705
- body: JSON.stringify(data),
706
- });
707
-
708
- if (!response.ok) {
709
- if (response.status === 401 && this.jwtToken) {
710
- this.jwtToken = null;
711
- return this.makePostRequest(path, data);
712
- }
713
- const errorText = await response.text();
714
- throw new Error(`HTTP ${response.status}: ${response.statusText}. Details: ${errorText}`);
715
- }
716
-
717
- return await response.json();
718
- }
719
-
720
- async makePutRequest(path, data) {
721
- const jwt = await this.authenticate();
722
- const url = `${this.config.baseUrl}/api/${this.config.projectId}${path}`;
723
-
724
- const response = await fetch(url, {
725
- method: 'PUT',
726
- headers: {
727
- 'Authorization': jwt,
728
- 'Content-Type': 'application/json',
729
- },
730
- body: JSON.stringify(data),
731
- });
732
-
733
- if (!response.ok) {
734
- if (response.status === 401 && this.jwtToken) {
735
- this.jwtToken = null;
736
- return this.makePutRequest(path, data);
737
- }
738
- const errorText = await response.text();
739
- throw new Error(`HTTP ${response.status}: ${response.statusText}. Details: ${errorText}`);
740
- }
741
-
742
- return await response.json();
743
- }
744
-
745
-
746
- escapeXml(text) {
747
- if (typeof text !== 'string') return text;
748
- return text
749
- .replace(/&/g, '&')
750
- .replace(/</g, '&lt;')
751
- .replace(/>/g, '&gt;')
752
- .replace(/"/g, '&quot;')
753
- .replace(/'/g, '&#39;');
754
- }
755
-
756
- formatValue(value, fieldName) {
757
- if (value === null || value === undefined) {
758
- return '';
759
- }
760
-
761
- // Handle arrays of simple values (tags, labels, etc.)
762
- if (Array.isArray(value)) {
763
- if (fieldName === 'tags') {
764
- return value.map(tag => `<tag>${this.escapeXml(tag)}</tag>`).join('');
765
- }
766
- if (fieldName === 'tests-ids') {
767
- return value.map(id => `<test_id>${id}</test_id>`).join('');
768
- }
769
- // Default array handling - use field name as tag (e.g., labels -> <label>)
770
- const singularFieldName = fieldName.endsWith('s') ? fieldName.slice(0, -1) : fieldName;
771
- return value.map(item => {
772
- if (typeof item === 'object' && item !== null) {
773
- return `<${singularFieldName}>${JSON.stringify(item)}</${singularFieldName}>`;
774
- }
775
- return `<${singularFieldName}>${this.escapeXml(item)}</${singularFieldName}>`;
776
- }).join('');
777
- }
778
-
779
- // Handle nested objects - stringify them without escaping
780
- if (typeof value === 'object') {
781
- return JSON.stringify(value);
782
- }
783
-
784
- // Handle strings that need escaping
785
- if (typeof value === 'string') {
786
- return this.escapeXml(value);
787
- }
788
-
789
- // Handle other primitives
790
- return String(value);
791
- }
792
-
793
- formatNestedObject(obj, fieldName) {
794
- if (fieldName === 'test' && obj.id) {
795
- // Special handling for test objects in testruns
796
- return `
797
- <id>${obj.id || ''}</id>
798
- <title>${this.escapeXml(obj.title || '')}</title>
799
- <priority>${obj.priority || 'normal'}</priority>
800
- <tags>${(obj.tags || []).map(tag => `<tag>${this.escapeXml(tag)}</tag>`).join('')}</tags>`;
801
- }
802
-
803
- // Generic object formatting
804
- return Object.entries(obj)
805
- .map(([key, value]) => `<${key}>${this.formatValue(value, key)}</${key}>`)
806
- .join('\n ');
807
- }
808
-
809
- formatModel(model, tagName, fields) {
810
- const attributes = model.attributes || {};
811
- const lines = [`<${tagName}>`];
812
-
813
- // Always include ID from root level
814
- lines.push(` <id>${model.id || ''}</id>`);
815
-
816
- // Process specified fields
817
- fields.forEach(field => {
818
- let value;
819
- let xmlFieldName = field;
820
-
821
- // Handle field mapping for hyphenated API fields
822
- if (field.includes('-')) {
823
- value = attributes[field];
824
- } else {
825
- // Try both versions for flexibility
826
- value = attributes[field] || attributes[field.replace('_', '-')];
827
- xmlFieldName = field.replace('-', '_');
828
- }
829
-
830
- const formattedValue = this.formatValue(value, field);
831
-
832
- if (field === 'test' && typeof value === 'object') {
833
- // Special case for nested test objects
834
- lines.push(` <test>${formattedValue}\n </test>`);
835
- } else {
836
- lines.push(` <${xmlFieldName}>${formattedValue}</${xmlFieldName}>`);
837
- }
838
- });
839
-
840
- lines.push(`</${tagName}>`);
841
- return lines.join('\n');
842
- }
843
-
844
- async getTests(filters = {}) {
845
- const params = this.buildSearchParams(filters);
846
- // Add labels=true and detail=true for comprehensive test information
847
- params.labels = 'true';
848
- params.detail = 'true';
849
-
850
- const data = await this.makeRequest('/tests', params);
851
- const formattedTests = data.data.map(test =>
852
- this.formatModel(test, 'test', [
853
- 'title', 'description', 'code', 'priority', 'state',
854
- 'suite-id', 'tags', 'file', 'jira-issues', 'assigned-to',
855
- 'created-at', 'updated-at', 'labels'
856
- ])
857
- ).join('\n\n');
858
-
859
- return {
860
- content: [
861
- {
862
- type: 'text',
863
- text: `Tests for project ${this.config.projectId}:\n\n${formattedTests}`,
864
- },
865
- ],
866
- };
867
- }
868
-
869
- async getTest(testId) {
870
- // Add labels=true and detail=true for comprehensive test information
871
- const data = await this.makeRequest(`/tests/${testId}`, { labels: 'true', detail: 'true' });
872
- const formattedTest = this.formatModel(data.data, 'test', [
873
- 'title', 'description', 'code', 'priority', 'state',
874
- 'suite-id', 'tags', 'file', 'jira-issues', 'assigned-to',
875
- 'created-at', 'updated-at', 'labels'
876
- ]);
877
-
878
- return {
879
- content: [
880
- {
881
- type: 'text',
882
- text: `Test ${testId}:\n\n${formattedTest}`,
883
- },
884
- ],
885
- };
886
- }
887
-
888
- buildSearchParams(filters = {}) {
889
- const params = {};
890
-
891
- // Handle basic filters
892
- Object.entries(filters).forEach(([key, value]) => {
893
- if (value !== undefined && value !== null) {
894
- if (key === 'labels' && Array.isArray(value)) {
895
- // Labels need special array handling
896
- value.forEach(label => {
897
- if (!params['labels[]']) {
898
- params['labels[]'] = [];
899
- }
900
- if (Array.isArray(params['labels[]'])) {
901
- params['labels[]'].push(label);
902
- } else {
903
- params['labels[]'] = [params['labels[]'], label];
904
- }
905
- });
906
- } else if (key === 'filter' && typeof value === 'object') {
907
- // Handle filter hash (e.g., filter[state]=manual)
908
- Object.entries(value).forEach(([filterKey, filterValue]) => {
909
- params[`filter[${filterKey}]`] = filterValue;
910
- });
911
- } else if (Array.isArray(value)) {
912
- // Handle other arrays
913
- value.forEach(v => {
914
- const paramKey = `${key}[]`;
915
- if (!params[paramKey]) {
916
- params[paramKey] = [];
917
- }
918
- if (Array.isArray(params[paramKey])) {
919
- params[paramKey].push(v);
920
- } else {
921
- params[paramKey] = [params[paramKey], v];
922
- }
923
- });
924
- } else {
925
- params[key] = String(value);
926
- }
927
- }
928
- });
929
-
930
- return params;
931
- }
932
-
933
- async searchTests(filters = {}) {
934
- const params = this.buildSearchParams(filters);
935
- // Add labels=true and detail=true for comprehensive test information
936
- params.labels = 'true';
937
- params.detail = 'true';
938
-
939
- const data = await this.makeRequest('/tests', params);
940
-
941
- const formattedTests = data.data.map(test =>
942
- this.formatModel(test, 'test', [
943
- 'title', 'description', 'code', 'priority', 'state',
944
- 'suite-id', 'tags', 'file', 'jira-issues', 'assigned-to',
945
- 'created-at', 'updated-at', 'labels'
946
- ])
947
- ).join('\n\n');
948
-
949
- const searchDescription = this.buildSearchDescription(filters);
950
-
951
- return {
952
- content: [
953
- {
954
- type: 'text',
955
- text: `Search results for tests${searchDescription}:\n\n${formattedTests || 'No tests found matching the criteria.'}`,
956
- },
957
- ],
958
- };
959
- }
960
-
961
- async searchSuites(filters = {}) {
962
- // Add filter=true for suites search to include tests
963
- const params = this.buildSearchParams({ ...filters, filter: true });
964
- const data = await this.makeRequest('/suites', params);
965
-
966
- const formattedSuites = data.data.map(suite =>
967
- this.formatModel(suite, 'suite', [
968
- 'title', 'description', 'test-count', 'is-root', 'file-type'
969
- ])
970
- ).join('\n\n');
971
-
972
- const searchDescription = this.buildSearchDescription(filters);
973
-
974
- return {
975
- content: [
976
- {
977
- type: 'text',
978
- text: `Search results for suites${searchDescription}:\n\n${formattedSuites || 'No suites found matching the criteria.'}`,
979
- },
980
- ],
981
- };
982
- }
983
-
984
- buildSearchDescription(filters) {
985
- const descriptions = [];
986
-
987
- if (filters.query) {
988
- if (filters.query.startsWith('@')) {
989
- descriptions.push(`tagged with "${filters.query}"`);
990
- } else if (filters.query.match(/^[A-Z]+-\d+$/)) {
991
- descriptions.push(`linked to Jira issue "${filters.query}"`);
992
- } else {
993
- descriptions.push(`containing "${filters.query}"`);
994
- }
995
- }
996
-
997
- if (filters.tql) {
998
- descriptions.push(`matching TQL: "${filters.tql}"`);
999
- }
1000
-
1001
- if (filters.labels && filters.labels.length > 0) {
1002
- descriptions.push(`with labels: ${filters.labels.join(', ')}`);
1003
- }
1004
-
1005
- if (filters.state) {
1006
- descriptions.push(`state: ${filters.state}`);
1007
- }
1008
-
1009
- if (filters.priority) {
1010
- descriptions.push(`priority: ${filters.priority}`);
1011
- }
1012
-
1013
- if (filters.filter && typeof filters.filter === 'object') {
1014
- const filterDesc = Object.entries(filters.filter)
1015
- .map(([key, value]) => `${key}: ${value}`)
1016
- .join(', ');
1017
- descriptions.push(`filtered by: ${filterDesc}`);
1018
- }
1019
-
1020
- return descriptions.length > 0 ? ` (${descriptions.join(', ')})` : '';
1021
- }
1022
-
1023
- async getRootSuites() {
1024
- const data = await this.makeRequest('/suites');
1025
- const formattedSuites = data.data.map(suite =>
1026
- this.formatModel(suite, 'suite', [
1027
- 'title', 'description', 'test-count', 'is-root', 'file-type'
1028
- ])
1029
- ).join('\n\n');
1030
-
1031
- return {
1032
- content: [
1033
- {
1034
- type: 'text',
1035
- text: `Root suites for project ${this.config.projectId}:\n\n${formattedSuites}`,
1036
- },
1037
- ],
1038
- };
1039
- }
1040
-
1041
- async getSuite(suiteId) {
1042
- const data = await this.makeRequest(`/suites/${suiteId}`);
1043
- const formattedSuite = this.formatModel(data.data, 'suite', [
1044
- 'title', 'description', 'test-count', 'is-root', 'file-type'
1045
- ]);
1046
-
1047
- // Format child suites and tests if they exist
1048
- let childContent = '';
1049
- if (data.data.relationships?.children?.data) {
1050
- const childSuites = data.data.relationships.children.data
1051
- .map(child => this.formatModel(child, 'suite', [
1052
- 'title', 'description', 'test-count', 'is-root', 'file-type'
1053
- ])).join('\n\n');
1054
- if (childSuites) {
1055
- childContent += `\n\nChild Suites:\n${childSuites}`;
1056
- }
1057
- }
1058
-
1059
- if (data.data.relationships?.tests?.data) {
1060
- const tests = data.data.relationships.tests.data
1061
- .map(test => this.formatModel(test, 'test', [
1062
- 'title', 'description', 'code', 'priority',
1063
- 'state', 'suite-id', 'tags', 'file'
1064
- ])).join('\n\n');
1065
- if (tests) {
1066
- childContent += `\n\nTests:\n${tests}`;
1067
- }
1068
- }
1069
-
1070
- return {
1071
- content: [
1072
- {
1073
- type: 'text',
1074
- text: `Suite ${suiteId}:\n\n${formattedSuite}${childContent}`,
1075
- },
1076
- ],
1077
- };
1078
- }
1079
-
1080
- async getRuns() {
1081
- const data = await this.makeRequest('/runs');
1082
- const formattedRuns = data.data.map(run =>
1083
- this.formatModel(run, 'run', [
1084
- 'status', 'title', 'tests-count', 'automated', 'duration',
1085
- 'passed', 'failed', 'skipped', 'created-at', 'finished-at'
1086
- ])
1087
- ).join('\n\n');
1088
-
1089
- return {
1090
- content: [
1091
- {
1092
- type: 'text',
1093
- text: `Test runs for project ${this.config.projectId}:\n\n${formattedRuns}`,
1094
- },
1095
- ],
1096
- };
1097
- }
1098
-
1099
- async getRun(runId, tree = false) {
1100
- const params = tree ? { tree: 'true' } : {};
1101
- const data = await this.makeRequest(`/runs/${runId}`, params);
1102
- const formattedRun = this.formatModel(data.data, 'run', [
1103
- 'status', 'title', 'tests-count', 'automated', 'duration',
1104
- 'passed', 'failed', 'skipped', 'created-at', 'finished-at'
1105
- ]);
1106
-
1107
- return {
1108
- content: [
1109
- {
1110
- type: 'text',
1111
- text: `Test run ${runId}:\n\n${formattedRun}`,
1112
- },
1113
- ],
1114
- };
1115
- }
1116
-
1117
- async getTestruns(testId, dateRange) {
1118
- const params = { test_id: testId };
1119
- if (dateRange) {
1120
- params.finished_at_date_range = dateRange;
1121
- }
1122
-
1123
- const data = await this.makeRequest('/testruns', params);
1124
- const formattedTestruns = data.data.map(testrun =>
1125
- this.formatModel(testrun, 'testrun', [
1126
- 'status', 'run-time', 'message', 'run-id', 'test'
1127
- ])
1128
- ).join('\n\n');
1129
-
1130
- return {
1131
- content: [
1132
- {
1133
- type: 'text',
1134
- text: `Test runs for test ${testId}:\n\n${formattedTestruns}`,
1135
- },
1136
- ],
1137
- };
1138
- }
1139
-
1140
- async getPlans(filters = {}) {
1141
- const data = await this.makeRequest('/plans', filters);
1142
- const formattedPlans = data.data.map(plan =>
1143
- this.formatModel(plan, 'plan', [
1144
- 'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
1145
- ])
1146
- ).join('\n\n');
1147
-
1148
- return {
1149
- content: [
1150
- {
1151
- type: 'text',
1152
- text: `Test plans for project ${this.config.projectId}:\n\n${formattedPlans}`,
1153
- },
1154
- ],
1155
- };
1156
- }
1157
-
1158
- async getPlan(planId) {
1159
- const data = await this.makeRequest(`/plans/${planId}`);
1160
- const formattedPlan = this.formatModel(data.data, 'plan', [
1161
- 'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
1162
- ]);
1163
-
1164
- return {
1165
- content: [
1166
- {
1167
- type: 'text',
1168
- text: `Test plan ${planId}:\n\n${formattedPlan}`,
1169
- },
1170
- ],
1171
- };
1172
- }
1173
-
1174
- async getLabels(filters = {}) {
1175
- const params = {};
1176
-
1177
- // Handle scope filter
1178
- if (filters.scope && Array.isArray(filters.scope)) {
1179
- if (!params['scope[]']) {
1180
- params['scope[]'] = [];
1181
- }
1182
- filters.scope.forEach(scope => {
1183
- params['scope[]'].push(scope);
1184
- });
1185
- }
1186
-
1187
- // Handle pagination
1188
- if (filters.page) {
1189
- params.page = filters.page;
1190
- }
1191
-
1192
- const data = await this.makeRequest('/labels', params);
1193
- const formattedLabels = data.data.map(label =>
1194
- this.formatModel(label, 'label', [
1195
- 'title', 'color', 'scope', 'visibility', 'field'
1196
- ])
1197
- ).join('\n\n');
1198
-
1199
- return {
1200
- content: [
1201
- {
1202
- type: 'text',
1203
- text: `Available labels for project ${this.config.projectId}:\n\n${formattedLabels || 'No labels found matching the criteria.'}`,
1204
- },
1205
- ],
1206
- };
1207
- }
1208
-
1209
- extractTagsFromTitle(title) {
1210
- if (!title) return [];
1211
-
1212
- // Find all @tags in the title
1213
- const tagMatches = title.match(/@([a-zA-Z0-9_\-]+)/g);
1214
-
1215
- if (!tagMatches) return [];
1216
-
1217
- // Remove @ prefix and deduplicate
1218
- const tags = tagMatches
1219
- .map(tag => tag.substring(1)) // Remove @
1220
- .filter(tag => tag.length > 0); // Filter out empty tags
1221
-
1222
- return [...new Set(tags)]; // Remove duplicates
1223
- }
1224
-
1225
- mergeTags(explicitTags, titleTags) {
1226
- const allTags = [];
1227
-
1228
- // Add explicit tags (remove @ prefix if present)
1229
- if (explicitTags && Array.isArray(explicitTags)) {
1230
- allTags.push(...explicitTags.map(tag => tag.replace(/^@/, '')));
1231
- }
1232
-
1233
- // Add tags extracted from title
1234
- if (titleTags && Array.isArray(titleTags)) {
1235
- allTags.push(...titleTags);
1236
- }
1237
-
1238
- // Remove duplicates while preserving order
1239
- const uniqueTags = [];
1240
- const seen = new Set();
1241
-
1242
- for (const tag of allTags) {
1243
- if (!seen.has(tag) && tag.length > 0) {
1244
- seen.add(tag);
1245
- uniqueTags.push(tag);
1246
- }
1247
- }
1248
-
1249
- return uniqueTags;
1250
- }
1251
-
1252
- async createTest(args) {
1253
- const { suite_id, labels_ids, fields, ...attributes } = args;
1254
-
1255
- // Extract tags from title if provided
1256
- const titleTags = this.extractTagsFromTitle(attributes.title);
1257
- const mergedTags = this.mergeTags(attributes.tags, titleTags);
1258
-
1259
- // Convert attributes to use hyphens instead of underscores for API compatibility
1260
- const apiAttributes = Object.fromEntries(
1261
- Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])
1262
- );
1263
-
1264
- // Add suite_id to attributes if provided
1265
- if (suite_id) {
1266
- apiAttributes['suite_id'] = suite_id;
1267
- }
1268
-
1269
- // Build JSON-API request data
1270
- const requestData = {
1271
- data: {
1272
- type: 'tests',
1273
- attributes: {
1274
- ...apiAttributes,
1275
- ...(mergedTags.length > 0 && { tags: mergedTags }),
1276
- ...(labels_ids && { labels_ids: labels_ids }),
1277
- ...(fields && { 'custom-fields': fields })
1278
- }
1279
- }
1280
- };
1281
-
1282
- const data = await this.makePostRequest('/tests', requestData);
1283
- const formattedTest = this.formatModel(data.data, 'test', [
1284
- 'title', 'description', 'code', 'priority',
1285
- 'state', 'suite-id', 'tags', 'file'
1286
- ]);
1287
-
1288
- return {
1289
- content: [
1290
- {
1291
- type: 'text',
1292
- text: `Successfully created test:\n\n${formattedTest}`,
1293
- },
1294
- ],
1295
- };
1296
- }
1297
-
1298
- async linkLabels(testId, labelsIds) {
1299
- if (!labelsIds || labelsIds.length === 0) {
1300
- return;
1301
- }
1302
-
1303
- // Process each label individually using the label linking API
1304
- for (const labelId of labelsIds) {
1305
- // Parse label:value format if present
1306
- let labelUid = labelId;
1307
- let value = null;
1308
-
1309
- if (labelId.includes(':')) {
1310
- [labelUid, value] = labelId.split(':', 2);
1311
- }
1312
-
1313
- // Build URL with test_id query parameter
1314
- let url = `/labels/${labelUid}/link?test_id=${testId}`;
1315
-
1316
- // Add value as query parameter if present
1317
- if (value) {
1318
- url += `&value=${encodeURIComponent(value)}`;
1319
- }
1320
-
1321
- await this.makePostRequest(url, {});
1322
- }
1323
- }
1324
-
1325
- async unlinkLabel(args) {
1326
- const { label_id, test_id, suite_id, value } = args;
1327
-
1328
- // Validate that either test_id or suite_id is provided
1329
- if (!test_id && !suite_id) {
1330
- throw new Error('Either test_id or suite_id must be provided');
1331
- }
1332
-
1333
- if (test_id && suite_id) {
1334
- throw new Error('Cannot specify both test_id and suite_id. Use one or the other.');
1335
- }
1336
-
1337
- // Build URL with base label link endpoint
1338
- let url = `/labels/${label_id}/link`;
1339
-
1340
- // Add appropriate query parameters
1341
- if (test_id) {
1342
- url += `?test_id=${test_id}`;
1343
- } else if (suite_id) {
1344
- url += `?suite_id=${suite_id}`;
1345
- }
1346
-
1347
- // Add event=remove parameter
1348
- url += `&event=remove`;
1349
-
1350
- // Add value as query parameter if present
1351
- if (value) {
1352
- url += `&value=${encodeURIComponent(value)}`;
1353
- }
1354
-
1355
- // Make the API request
1356
- await this.makePostRequest(url, {});
1357
-
1358
- const itemType = test_id ? 'test' : 'suite';
1359
- const itemId = test_id || suite_id;
1360
- const removeDescription = value ? `value "${value}"` : 'all instances';
1361
-
1362
- return {
1363
- content: [
1364
- {
1365
- type: 'text',
1366
- text: `Successfully removed label "${label_id}" (${removeDescription}) from ${itemType} "${itemId}"`,
1367
- },
1368
- ],
1369
- };
1370
- }
1371
-
1372
- async updateTest(args) {
1373
- const { test_id, suite_id, labels_ids, fields, ...attributes } = args;
1374
-
1375
- // Extract tags from title if provided
1376
- const titleTags = this.extractTagsFromTitle(attributes.title);
1377
- const mergedTags = this.mergeTags(attributes.tags, titleTags);
1378
-
1379
- // Convert attributes to use hyphens instead of underscores for API compatibility
1380
- const apiAttributes = Object.fromEntries(
1381
- Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])
1382
- );
1383
-
1384
- // Add suite_id to attributes if provided
1385
- if (suite_id) {
1386
- apiAttributes['suite_id'] = suite_id;
1387
- }
1388
-
1389
- let data;
1390
-
1391
- // Handle regular test attributes update using JSON-API format
1392
- const requestData = {
1393
- data: {
1394
- id: test_id,
1395
- type: 'tests',
1396
- attributes: {
1397
- ...apiAttributes,
1398
- ...(mergedTags.length > 0 && { tags: mergedTags }),
1399
- ...(fields && { 'custom-fields': fields })
1400
- }
1401
- }
1402
- };
1403
-
1404
- data = await this.makePutRequest(`/tests/${test_id}`, requestData);
1405
-
1406
- // Handle labels_ids using the label linking API
1407
- if (labels_ids && labels_ids.length > 0) {
1408
- await this.linkLabels(test_id, labels_ids);
1409
-
1410
- // After linking labels, fetch the updated test to reflect changes
1411
- data = await this.makeRequest(`/tests/${test_id}`);
1412
- }
1413
-
1414
- const formattedTest = this.formatModel(data.data, 'test', [
1415
- 'title', 'description', 'code', 'priority',
1416
- 'state', 'suite-id', 'tags', 'file'
1417
- ]);
1418
-
1419
- return {
1420
- content: [
1421
- {
1422
- type: 'text',
1423
- text: `Successfully updated test:\n\n${formattedTest}`,
1424
- },
1425
- ],
1426
- };
1427
- }
1428
-
1429
- async createSuite(args) {
1430
- const { parent_id, fields, ...attributes } = args;
1431
- const requestData = {
1432
- data: {
1433
- type: 'suites',
1434
- attributes: {
1435
- ...Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
1436
- 'file-type': 'file',
1437
- ...(fields && { 'custom-fields': fields })
1438
- }
1439
- }
1440
- };
1441
-
1442
- if (parent_id) {
1443
- requestData.data.relationships = {
1444
- parent: {
1445
- data: {
1446
- type: 'suites',
1447
- id: parent_id
1448
- }
1449
- }
1450
- };
1451
- }
1452
-
1453
- const data = await this.makePostRequest('/suites', requestData);
1454
- const formattedSuite = this.formatModel(data.data, 'suite', [
1455
- 'title', 'description', 'test-count', 'is-root', 'file-type'
1456
- ]);
1457
-
1458
- return {
1459
- content: [
1460
- {
1461
- type: 'text',
1462
- text: `Successfully created suite:\n\n${formattedSuite}`,
1463
- },
1464
- ],
1465
- };
1466
- }
1467
-
1468
- async createFolder(args) {
1469
- const { parent_id, fields, ...attributes } = args;
1470
- const requestData = {
1471
- data: {
1472
- type: 'suites',
1473
- attributes: {
1474
- ...Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
1475
- 'file-type': 'folder',
1476
- ...(fields && { 'custom-fields': fields })
1477
- }
1478
- }
1479
- };
1480
-
1481
- if (parent_id) {
1482
- requestData.data.relationships = {
1483
- parent: {
1484
- data: {
1485
- type: 'suites',
1486
- id: parent_id
1487
- }
1488
- }
1489
- };
1490
- }
1491
-
1492
- const data = await this.makePostRequest('/suites', requestData);
1493
- const formattedSuite = this.formatModel(data.data, 'suite', [
1494
- 'title', 'description', 'test-count', 'is-root', 'file-type'
1495
- ]);
1496
-
1497
- return {
1498
- content: [
1499
- {
1500
- type: 'text',
1501
- text: `Successfully created folder:\n\n${formattedSuite}`,
1502
- },
1503
- ],
1504
- };
1505
- }
1506
-
1507
- async createLabel(args) {
1508
- const { title, color, scope, visibility, field, ...otherAttributes } = args;
1509
-
1510
- const requestData = {
1511
- data: {
1512
- type: 'labels',
1513
- attributes: {
1514
- ...Object.fromEntries(Object.entries(otherAttributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
1515
- title,
1516
- color,
1517
- scope,
1518
- visibility
1519
- }
1520
- }
1521
- };
1522
-
1523
- // Add field configuration if provided
1524
- if (field) {
1525
- requestData.data.attributes.field = {
1526
- type: field.type,
1527
- ...(field.short !== undefined && { short: field.short }),
1528
- ...(field.value && { value: field.value })
1529
- };
1530
- }
1531
-
1532
- const data = await this.makePostRequest('/labels', requestData);
1533
- const formattedLabel = this.formatModel(data.data, 'label', [
1534
- 'title', 'color', 'scope', 'visibility', 'field'
1535
- ]);
1536
-
1537
- return {
1538
- content: [
1539
- {
1540
- type: 'text',
1541
- text: `Successfully created label:\n\n${formattedLabel}`,
1542
- },
1543
- ],
1544
- };
1545
- }
1546
-
1547
- async run() {
1548
- // Test authentication on startup
1549
- try {
1550
- await this.authenticate();
1551
- console.error('✓ Successfully authenticated with Testomatio API');
1552
- } catch (error) {
1553
- console.error('✗ Authentication failed:', error.message);
1554
- process.exit(1);
1555
- }
1556
-
1557
- const transport = new StdioServerTransport();
1558
- await this.server.connect(transport);
1559
- console.error('Testomatio MCP server running on stdio');
1560
- }
1561
- }
1562
-
1563
- // Export the class for testing
1564
- export { TestomatioMCPServer };
1565
-
1566
- // Parse command line arguments using commander
1567
- function parseArgs() {
1568
- program
1569
- .name('testomatio-mcp')
1570
- .description('Model Context Protocol server for Testomatio API')
1571
- .version('1.0.0')
1572
- .option('-t, --token <token>', 'Testomatio API token')
1573
- .option('-p, --project <project>', 'Project ID')
1574
- .option('--base-url <url>', 'Base URL for Testomatio API', 'https://app.testomat.io')
1575
- .parse();
1576
-
1577
- const options = program.opts();
1578
-
1579
- const token = normalizeString(options.token || process.env.TESTOMATIO_API_TOKEN);
1580
- const projectId = normalizeString(options.project || process.env.TESTOMATIO_PROJECT_ID);
1581
- const baseUrl = normalizeBaseUrl(
1582
- options.baseUrl ||
1583
- process.env.TESTOMATIO_BASE_URL ||
1584
- 'https://app.testomat.io'
1585
- );
1586
-
1587
- if (!token) {
1588
- console.error('Error: API token is required. Use --token <token> or set TESTOMATIO_API_TOKEN environment variable');
1589
- process.exit(1);
1590
- }
1591
-
1592
- if (!projectId) {
1593
- console.error('Error: Project ID is required. Use --project <project_id> or set TESTOMATIO_PROJECT_ID environment variable');
1594
- process.exit(1);
1595
- }
1596
-
1597
- return { token, projectId, baseUrl };
1598
- }
1599
-
1600
- // Main execution
1601
- async function main() {
1602
- try {
1603
- const config = parseArgs();
1604
- const server = new TestomatioMCPServer(config);
1605
- await server.run();
1606
- } catch (error) {
1607
- console.error('Failed to start server:', error);
4
+ import { main } from './src/cli/main.js';
5
+
6
+ export { TestomatioMCPServer } from './src/mcp/server.js';
7
+
8
+ const modulePath = fileURLToPath(import.meta.url);
9
+ const executedPath = process.argv[1] || '';
10
+ const isDirectExecution =
11
+ executedPath === modulePath ||
12
+ executedPath.endsWith('/index.js') ||
13
+ executedPath.endsWith('\\index.js') ||
14
+ executedPath.endsWith('/testomatio-mcp') ||
15
+ executedPath.endsWith('\\testomatio-mcp');
16
+
17
+ if (isDirectExecution) {
18
+ main(process.argv).catch((error) => {
19
+ console.error('Fatal startup error:', error.message || error);
1608
20
  process.exit(1);
1609
- }
1610
- }
1611
-
1612
- // Run main() when executed directly or as a bin script
1613
- // Don't run when imported as a module for testing
1614
- if (import.meta.url.startsWith('file://') && process.argv[1]) {
1615
- const modulePath = fileURLToPath(import.meta.url);
1616
- const isDirectExecution = process.argv[1].includes('index.js') ||
1617
- process.argv[1] === modulePath ||
1618
- process.argv[1].endsWith('/testomatio-mcp');
1619
-
1620
- if (isDirectExecution) {
1621
- main().catch(console.error);
1622
- }
21
+ });
1623
22
  }