@testomatio/mcp 1.0.13 → 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,1627 +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
- oneOf: [
543
- { required: ['test_id'] },
544
- { required: ['suite_id'] }
545
- ],
546
- },
547
- },
548
- {
549
- name: 'create_label',
550
- description: 'Create a new label with optional custom field configuration. Labels can be used to tag and categorize tests and suites',
551
- inputSchema: {
552
- type: 'object',
553
- properties: {
554
- title: {
555
- type: 'string',
556
- description: 'Label title (e.g., "Severity", "Priority", "Type")',
557
- },
558
- color: {
559
- type: 'string',
560
- description: 'Label color in hex format (e.g., "#ffe9ad")',
561
- },
562
- scope: {
563
- type: 'array',
564
- items: {
565
- type: 'string',
566
- enum: ['tests', 'suites']
567
- },
568
- description: 'Where this label can be used (e.g., ["tests", "suites"])',
569
- },
570
- visibility: {
571
- type: 'array',
572
- items: {
573
- type: 'string'
574
- },
575
- description: 'Where the label is visible (e.g., ["list"])',
576
- },
577
- field: {
578
- type: 'object',
579
- description: 'Custom field configuration for labels with predefined values',
580
- properties: {
581
- type: {
582
- type: 'string',
583
- description: 'Field type (e.g., "list", "string", "number")',
584
- },
585
- short: {
586
- type: 'boolean',
587
- description: 'Whether to display short version',
588
- },
589
- value: {
590
- type: 'string',
591
- description: 'Predefined values for the field (newline separated for list type)',
592
- },
593
- },
594
- required: ['type'],
595
- },
596
- },
597
- required: ['title'],
598
- },
599
- },
600
- ],
601
- };
602
- });
603
-
604
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
605
- const { name, arguments: args } = request.params;
606
-
607
- try {
608
- switch (name) {
609
- case 'get_tests':
610
- return await this.getTests(args);
611
- case 'get_test':
612
- return await this.getTest(args.test_id);
613
- case 'search_tests':
614
- return await this.searchTests(args);
615
- case 'search_suites':
616
- return await this.searchSuites(args);
617
- case 'get_root_suites':
618
- return await this.getRootSuites();
619
- case 'get_suite':
620
- return await this.getSuite(args.suite_id);
621
- case 'get_runs':
622
- return await this.getRuns();
623
- case 'get_run':
624
- return await this.getRun(args.run_id, args.tree);
625
- case 'get_testruns':
626
- return await this.getTestruns(args.test_id, args.finished_at_date_range);
627
- case 'get_plans':
628
- return await this.getPlans(args);
629
- case 'get_plan':
630
- return await this.getPlan(args.plan_id);
631
- case 'get_labels':
632
- return await this.getLabels(args);
633
- case 'unlink_label':
634
- return await this.unlinkLabel(args);
635
- case 'create_test':
636
- return await this.createTest(args);
637
- case 'update_test':
638
- return await this.updateTest(args);
639
- case 'create_suite':
640
- return await this.createSuite(args);
641
- case 'create_folder':
642
- return await this.createFolder(args);
643
- case 'create_label':
644
- return await this.createLabel(args);
645
- default:
646
- throw new Error(`Unknown tool: ${name}`);
647
- }
648
- } catch (error) {
649
- return {
650
- content: [
651
- {
652
- type: 'text',
653
- text: `Error: ${error.message}`,
654
- },
655
- ],
656
- };
657
- }
658
- });
659
- }
660
-
661
- async makeRequest(path, params = {}) {
662
- // Ensure we have a valid JWT token
663
- const jwt = await this.authenticate();
664
-
665
- const url = new URL(`${this.config.baseUrl}/api/${this.config.projectId}${path}`);
666
-
667
- // Add query parameters with proper array handling
668
- Object.entries(params).forEach(([key, value]) => {
669
- if (value !== undefined && value !== null) {
670
- if (Array.isArray(value)) {
671
- // Handle arrays (e.g., labels[])
672
- value.forEach(v => url.searchParams.append(key, v));
673
- } else {
674
- url.searchParams.append(key, String(value));
675
- }
676
- }
677
- });
678
-
679
- const response = await fetch(url.toString(), {
680
- method: 'GET',
681
- headers: {
682
- 'Authorization': jwt,
683
- 'Content-Type': 'application/json',
684
- },
685
- });
686
-
687
- if (!response.ok) {
688
- // If unauthorized, clear the JWT token and retry once
689
- if (response.status === 401 && this.jwtToken) {
690
- this.jwtToken = null;
691
- return this.makeRequest(path, params);
692
- }
693
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
694
- }
695
-
696
- return await response.json();
697
- }
698
-
699
- async makePostRequest(path, data) {
700
- const jwt = await this.authenticate();
701
- const url = `${this.config.baseUrl}/api/${this.config.projectId}${path}`;
702
-
703
- const response = await fetch(url, {
704
- method: 'POST',
705
- headers: {
706
- 'Authorization': jwt,
707
- 'Content-Type': 'application/json',
708
- },
709
- body: JSON.stringify(data),
710
- });
711
-
712
- if (!response.ok) {
713
- if (response.status === 401 && this.jwtToken) {
714
- this.jwtToken = null;
715
- return this.makePostRequest(path, data);
716
- }
717
- const errorText = await response.text();
718
- throw new Error(`HTTP ${response.status}: ${response.statusText}. Details: ${errorText}`);
719
- }
720
-
721
- return await response.json();
722
- }
723
-
724
- async makePutRequest(path, data) {
725
- const jwt = await this.authenticate();
726
- const url = `${this.config.baseUrl}/api/${this.config.projectId}${path}`;
727
-
728
- const response = await fetch(url, {
729
- method: 'PUT',
730
- headers: {
731
- 'Authorization': jwt,
732
- 'Content-Type': 'application/json',
733
- },
734
- body: JSON.stringify(data),
735
- });
736
-
737
- if (!response.ok) {
738
- if (response.status === 401 && this.jwtToken) {
739
- this.jwtToken = null;
740
- return this.makePutRequest(path, data);
741
- }
742
- const errorText = await response.text();
743
- throw new Error(`HTTP ${response.status}: ${response.statusText}. Details: ${errorText}`);
744
- }
745
-
746
- return await response.json();
747
- }
748
-
749
-
750
- escapeXml(text) {
751
- if (typeof text !== 'string') return text;
752
- return text
753
- .replace(/&/g, '&')
754
- .replace(/</g, '&lt;')
755
- .replace(/>/g, '&gt;')
756
- .replace(/"/g, '&quot;')
757
- .replace(/'/g, '&#39;');
758
- }
759
-
760
- formatValue(value, fieldName) {
761
- if (value === null || value === undefined) {
762
- return '';
763
- }
764
-
765
- // Handle arrays of simple values (tags, labels, etc.)
766
- if (Array.isArray(value)) {
767
- if (fieldName === 'tags') {
768
- return value.map(tag => `<tag>${this.escapeXml(tag)}</tag>`).join('');
769
- }
770
- if (fieldName === 'tests-ids') {
771
- return value.map(id => `<test_id>${id}</test_id>`).join('');
772
- }
773
- // Default array handling - use field name as tag (e.g., labels -> <label>)
774
- const singularFieldName = fieldName.endsWith('s') ? fieldName.slice(0, -1) : fieldName;
775
- return value.map(item => {
776
- if (typeof item === 'object' && item !== null) {
777
- return `<${singularFieldName}>${JSON.stringify(item)}</${singularFieldName}>`;
778
- }
779
- return `<${singularFieldName}>${this.escapeXml(item)}</${singularFieldName}>`;
780
- }).join('');
781
- }
782
-
783
- // Handle nested objects - stringify them without escaping
784
- if (typeof value === 'object') {
785
- return JSON.stringify(value);
786
- }
787
-
788
- // Handle strings that need escaping
789
- if (typeof value === 'string') {
790
- return this.escapeXml(value);
791
- }
792
-
793
- // Handle other primitives
794
- return String(value);
795
- }
796
-
797
- formatNestedObject(obj, fieldName) {
798
- if (fieldName === 'test' && obj.id) {
799
- // Special handling for test objects in testruns
800
- return `
801
- <id>${obj.id || ''}</id>
802
- <title>${this.escapeXml(obj.title || '')}</title>
803
- <priority>${obj.priority || 'normal'}</priority>
804
- <tags>${(obj.tags || []).map(tag => `<tag>${this.escapeXml(tag)}</tag>`).join('')}</tags>`;
805
- }
806
-
807
- // Generic object formatting
808
- return Object.entries(obj)
809
- .map(([key, value]) => `<${key}>${this.formatValue(value, key)}</${key}>`)
810
- .join('\n ');
811
- }
812
-
813
- formatModel(model, tagName, fields) {
814
- const attributes = model.attributes || {};
815
- const lines = [`<${tagName}>`];
816
-
817
- // Always include ID from root level
818
- lines.push(` <id>${model.id || ''}</id>`);
819
-
820
- // Process specified fields
821
- fields.forEach(field => {
822
- let value;
823
- let xmlFieldName = field;
824
-
825
- // Handle field mapping for hyphenated API fields
826
- if (field.includes('-')) {
827
- value = attributes[field];
828
- } else {
829
- // Try both versions for flexibility
830
- value = attributes[field] || attributes[field.replace('_', '-')];
831
- xmlFieldName = field.replace('-', '_');
832
- }
833
-
834
- const formattedValue = this.formatValue(value, field);
835
-
836
- if (field === 'test' && typeof value === 'object') {
837
- // Special case for nested test objects
838
- lines.push(` <test>${formattedValue}\n </test>`);
839
- } else {
840
- lines.push(` <${xmlFieldName}>${formattedValue}</${xmlFieldName}>`);
841
- }
842
- });
843
-
844
- lines.push(`</${tagName}>`);
845
- return lines.join('\n');
846
- }
847
-
848
- async getTests(filters = {}) {
849
- const params = this.buildSearchParams(filters);
850
- // Add labels=true and detail=true for comprehensive test information
851
- params.labels = 'true';
852
- params.detail = 'true';
853
-
854
- const data = await this.makeRequest('/tests', params);
855
- const formattedTests = data.data.map(test =>
856
- this.formatModel(test, 'test', [
857
- 'title', 'description', 'code', 'priority', 'state',
858
- 'suite-id', 'tags', 'file', 'jira-issues', 'assigned-to',
859
- 'created-at', 'updated-at', 'labels'
860
- ])
861
- ).join('\n\n');
862
-
863
- return {
864
- content: [
865
- {
866
- type: 'text',
867
- text: `Tests for project ${this.config.projectId}:\n\n${formattedTests}`,
868
- },
869
- ],
870
- };
871
- }
872
-
873
- async getTest(testId) {
874
- // Add labels=true and detail=true for comprehensive test information
875
- const data = await this.makeRequest(`/tests/${testId}`, { labels: 'true', detail: 'true' });
876
- const formattedTest = this.formatModel(data.data, 'test', [
877
- 'title', 'description', 'code', 'priority', 'state',
878
- 'suite-id', 'tags', 'file', 'jira-issues', 'assigned-to',
879
- 'created-at', 'updated-at', 'labels'
880
- ]);
881
-
882
- return {
883
- content: [
884
- {
885
- type: 'text',
886
- text: `Test ${testId}:\n\n${formattedTest}`,
887
- },
888
- ],
889
- };
890
- }
891
-
892
- buildSearchParams(filters = {}) {
893
- const params = {};
894
-
895
- // Handle basic filters
896
- Object.entries(filters).forEach(([key, value]) => {
897
- if (value !== undefined && value !== null) {
898
- if (key === 'labels' && Array.isArray(value)) {
899
- // Labels need special array handling
900
- value.forEach(label => {
901
- if (!params['labels[]']) {
902
- params['labels[]'] = [];
903
- }
904
- if (Array.isArray(params['labels[]'])) {
905
- params['labels[]'].push(label);
906
- } else {
907
- params['labels[]'] = [params['labels[]'], label];
908
- }
909
- });
910
- } else if (key === 'filter' && typeof value === 'object') {
911
- // Handle filter hash (e.g., filter[state]=manual)
912
- Object.entries(value).forEach(([filterKey, filterValue]) => {
913
- params[`filter[${filterKey}]`] = filterValue;
914
- });
915
- } else if (Array.isArray(value)) {
916
- // Handle other arrays
917
- value.forEach(v => {
918
- const paramKey = `${key}[]`;
919
- if (!params[paramKey]) {
920
- params[paramKey] = [];
921
- }
922
- if (Array.isArray(params[paramKey])) {
923
- params[paramKey].push(v);
924
- } else {
925
- params[paramKey] = [params[paramKey], v];
926
- }
927
- });
928
- } else {
929
- params[key] = String(value);
930
- }
931
- }
932
- });
933
-
934
- return params;
935
- }
936
-
937
- async searchTests(filters = {}) {
938
- const params = this.buildSearchParams(filters);
939
- // Add labels=true and detail=true for comprehensive test information
940
- params.labels = 'true';
941
- params.detail = 'true';
942
-
943
- const data = await this.makeRequest('/tests', params);
944
-
945
- const formattedTests = data.data.map(test =>
946
- this.formatModel(test, 'test', [
947
- 'title', 'description', 'code', 'priority', 'state',
948
- 'suite-id', 'tags', 'file', 'jira-issues', 'assigned-to',
949
- 'created-at', 'updated-at', 'labels'
950
- ])
951
- ).join('\n\n');
952
-
953
- const searchDescription = this.buildSearchDescription(filters);
954
-
955
- return {
956
- content: [
957
- {
958
- type: 'text',
959
- text: `Search results for tests${searchDescription}:\n\n${formattedTests || 'No tests found matching the criteria.'}`,
960
- },
961
- ],
962
- };
963
- }
964
-
965
- async searchSuites(filters = {}) {
966
- // Add filter=true for suites search to include tests
967
- const params = this.buildSearchParams({ ...filters, filter: true });
968
- const data = await this.makeRequest('/suites', params);
969
-
970
- const formattedSuites = data.data.map(suite =>
971
- this.formatModel(suite, 'suite', [
972
- 'title', 'description', 'test-count', 'is-root', 'file-type'
973
- ])
974
- ).join('\n\n');
975
-
976
- const searchDescription = this.buildSearchDescription(filters);
977
-
978
- return {
979
- content: [
980
- {
981
- type: 'text',
982
- text: `Search results for suites${searchDescription}:\n\n${formattedSuites || 'No suites found matching the criteria.'}`,
983
- },
984
- ],
985
- };
986
- }
987
-
988
- buildSearchDescription(filters) {
989
- const descriptions = [];
990
-
991
- if (filters.query) {
992
- if (filters.query.startsWith('@')) {
993
- descriptions.push(`tagged with "${filters.query}"`);
994
- } else if (filters.query.match(/^[A-Z]+-\d+$/)) {
995
- descriptions.push(`linked to Jira issue "${filters.query}"`);
996
- } else {
997
- descriptions.push(`containing "${filters.query}"`);
998
- }
999
- }
1000
-
1001
- if (filters.tql) {
1002
- descriptions.push(`matching TQL: "${filters.tql}"`);
1003
- }
1004
-
1005
- if (filters.labels && filters.labels.length > 0) {
1006
- descriptions.push(`with labels: ${filters.labels.join(', ')}`);
1007
- }
1008
-
1009
- if (filters.state) {
1010
- descriptions.push(`state: ${filters.state}`);
1011
- }
1012
-
1013
- if (filters.priority) {
1014
- descriptions.push(`priority: ${filters.priority}`);
1015
- }
1016
-
1017
- if (filters.filter && typeof filters.filter === 'object') {
1018
- const filterDesc = Object.entries(filters.filter)
1019
- .map(([key, value]) => `${key}: ${value}`)
1020
- .join(', ');
1021
- descriptions.push(`filtered by: ${filterDesc}`);
1022
- }
1023
-
1024
- return descriptions.length > 0 ? ` (${descriptions.join(', ')})` : '';
1025
- }
1026
-
1027
- async getRootSuites() {
1028
- const data = await this.makeRequest('/suites');
1029
- const formattedSuites = data.data.map(suite =>
1030
- this.formatModel(suite, 'suite', [
1031
- 'title', 'description', 'test-count', 'is-root', 'file-type'
1032
- ])
1033
- ).join('\n\n');
1034
-
1035
- return {
1036
- content: [
1037
- {
1038
- type: 'text',
1039
- text: `Root suites for project ${this.config.projectId}:\n\n${formattedSuites}`,
1040
- },
1041
- ],
1042
- };
1043
- }
1044
-
1045
- async getSuite(suiteId) {
1046
- const data = await this.makeRequest(`/suites/${suiteId}`);
1047
- const formattedSuite = this.formatModel(data.data, 'suite', [
1048
- 'title', 'description', 'test-count', 'is-root', 'file-type'
1049
- ]);
1050
-
1051
- // Format child suites and tests if they exist
1052
- let childContent = '';
1053
- if (data.data.relationships?.children?.data) {
1054
- const childSuites = data.data.relationships.children.data
1055
- .map(child => this.formatModel(child, 'suite', [
1056
- 'title', 'description', 'test-count', 'is-root', 'file-type'
1057
- ])).join('\n\n');
1058
- if (childSuites) {
1059
- childContent += `\n\nChild Suites:\n${childSuites}`;
1060
- }
1061
- }
1062
-
1063
- if (data.data.relationships?.tests?.data) {
1064
- const tests = data.data.relationships.tests.data
1065
- .map(test => this.formatModel(test, 'test', [
1066
- 'title', 'description', 'code', 'priority',
1067
- 'state', 'suite-id', 'tags', 'file'
1068
- ])).join('\n\n');
1069
- if (tests) {
1070
- childContent += `\n\nTests:\n${tests}`;
1071
- }
1072
- }
1073
-
1074
- return {
1075
- content: [
1076
- {
1077
- type: 'text',
1078
- text: `Suite ${suiteId}:\n\n${formattedSuite}${childContent}`,
1079
- },
1080
- ],
1081
- };
1082
- }
1083
-
1084
- async getRuns() {
1085
- const data = await this.makeRequest('/runs');
1086
- const formattedRuns = data.data.map(run =>
1087
- this.formatModel(run, 'run', [
1088
- 'status', 'title', 'tests-count', 'automated', 'duration',
1089
- 'passed', 'failed', 'skipped', 'created-at', 'finished-at'
1090
- ])
1091
- ).join('\n\n');
1092
-
1093
- return {
1094
- content: [
1095
- {
1096
- type: 'text',
1097
- text: `Test runs for project ${this.config.projectId}:\n\n${formattedRuns}`,
1098
- },
1099
- ],
1100
- };
1101
- }
1102
-
1103
- async getRun(runId, tree = false) {
1104
- const params = tree ? { tree: 'true' } : {};
1105
- const data = await this.makeRequest(`/runs/${runId}`, params);
1106
- const formattedRun = this.formatModel(data.data, 'run', [
1107
- 'status', 'title', 'tests-count', 'automated', 'duration',
1108
- 'passed', 'failed', 'skipped', 'created-at', 'finished-at'
1109
- ]);
1110
-
1111
- return {
1112
- content: [
1113
- {
1114
- type: 'text',
1115
- text: `Test run ${runId}:\n\n${formattedRun}`,
1116
- },
1117
- ],
1118
- };
1119
- }
1120
-
1121
- async getTestruns(testId, dateRange) {
1122
- const params = { test_id: testId };
1123
- if (dateRange) {
1124
- params.finished_at_date_range = dateRange;
1125
- }
1126
-
1127
- const data = await this.makeRequest('/testruns', params);
1128
- const formattedTestruns = data.data.map(testrun =>
1129
- this.formatModel(testrun, 'testrun', [
1130
- 'status', 'run-time', 'message', 'run-id', 'test'
1131
- ])
1132
- ).join('\n\n');
1133
-
1134
- return {
1135
- content: [
1136
- {
1137
- type: 'text',
1138
- text: `Test runs for test ${testId}:\n\n${formattedTestruns}`,
1139
- },
1140
- ],
1141
- };
1142
- }
1143
-
1144
- async getPlans(filters = {}) {
1145
- const data = await this.makeRequest('/plans', filters);
1146
- const formattedPlans = data.data.map(plan =>
1147
- this.formatModel(plan, 'plan', [
1148
- 'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
1149
- ])
1150
- ).join('\n\n');
1151
-
1152
- return {
1153
- content: [
1154
- {
1155
- type: 'text',
1156
- text: `Test plans for project ${this.config.projectId}:\n\n${formattedPlans}`,
1157
- },
1158
- ],
1159
- };
1160
- }
1161
-
1162
- async getPlan(planId) {
1163
- const data = await this.makeRequest(`/plans/${planId}`);
1164
- const formattedPlan = this.formatModel(data.data, 'plan', [
1165
- 'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
1166
- ]);
1167
-
1168
- return {
1169
- content: [
1170
- {
1171
- type: 'text',
1172
- text: `Test plan ${planId}:\n\n${formattedPlan}`,
1173
- },
1174
- ],
1175
- };
1176
- }
1177
-
1178
- async getLabels(filters = {}) {
1179
- const params = {};
1180
-
1181
- // Handle scope filter
1182
- if (filters.scope && Array.isArray(filters.scope)) {
1183
- if (!params['scope[]']) {
1184
- params['scope[]'] = [];
1185
- }
1186
- filters.scope.forEach(scope => {
1187
- params['scope[]'].push(scope);
1188
- });
1189
- }
1190
-
1191
- // Handle pagination
1192
- if (filters.page) {
1193
- params.page = filters.page;
1194
- }
1195
-
1196
- const data = await this.makeRequest('/labels', params);
1197
- const formattedLabels = data.data.map(label =>
1198
- this.formatModel(label, 'label', [
1199
- 'title', 'color', 'scope', 'visibility', 'field'
1200
- ])
1201
- ).join('\n\n');
1202
-
1203
- return {
1204
- content: [
1205
- {
1206
- type: 'text',
1207
- text: `Available labels for project ${this.config.projectId}:\n\n${formattedLabels || 'No labels found matching the criteria.'}`,
1208
- },
1209
- ],
1210
- };
1211
- }
1212
-
1213
- extractTagsFromTitle(title) {
1214
- if (!title) return [];
1215
-
1216
- // Find all @tags in the title
1217
- const tagMatches = title.match(/@([a-zA-Z0-9_\-]+)/g);
1218
-
1219
- if (!tagMatches) return [];
1220
-
1221
- // Remove @ prefix and deduplicate
1222
- const tags = tagMatches
1223
- .map(tag => tag.substring(1)) // Remove @
1224
- .filter(tag => tag.length > 0); // Filter out empty tags
1225
-
1226
- return [...new Set(tags)]; // Remove duplicates
1227
- }
1228
-
1229
- mergeTags(explicitTags, titleTags) {
1230
- const allTags = [];
1231
-
1232
- // Add explicit tags (remove @ prefix if present)
1233
- if (explicitTags && Array.isArray(explicitTags)) {
1234
- allTags.push(...explicitTags.map(tag => tag.replace(/^@/, '')));
1235
- }
1236
-
1237
- // Add tags extracted from title
1238
- if (titleTags && Array.isArray(titleTags)) {
1239
- allTags.push(...titleTags);
1240
- }
1241
-
1242
- // Remove duplicates while preserving order
1243
- const uniqueTags = [];
1244
- const seen = new Set();
1245
-
1246
- for (const tag of allTags) {
1247
- if (!seen.has(tag) && tag.length > 0) {
1248
- seen.add(tag);
1249
- uniqueTags.push(tag);
1250
- }
1251
- }
1252
-
1253
- return uniqueTags;
1254
- }
1255
-
1256
- async createTest(args) {
1257
- const { suite_id, labels_ids, fields, ...attributes } = args;
1258
-
1259
- // Extract tags from title if provided
1260
- const titleTags = this.extractTagsFromTitle(attributes.title);
1261
- const mergedTags = this.mergeTags(attributes.tags, titleTags);
1262
-
1263
- // Convert attributes to use hyphens instead of underscores for API compatibility
1264
- const apiAttributes = Object.fromEntries(
1265
- Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])
1266
- );
1267
-
1268
- // Add suite_id to attributes if provided
1269
- if (suite_id) {
1270
- apiAttributes['suite_id'] = suite_id;
1271
- }
1272
-
1273
- // Build JSON-API request data
1274
- const requestData = {
1275
- data: {
1276
- type: 'tests',
1277
- attributes: {
1278
- ...apiAttributes,
1279
- ...(mergedTags.length > 0 && { tags: mergedTags }),
1280
- ...(labels_ids && { labels_ids: labels_ids }),
1281
- ...(fields && { 'custom-fields': fields })
1282
- }
1283
- }
1284
- };
1285
-
1286
- const data = await this.makePostRequest('/tests', requestData);
1287
- const formattedTest = this.formatModel(data.data, 'test', [
1288
- 'title', 'description', 'code', 'priority',
1289
- 'state', 'suite-id', 'tags', 'file'
1290
- ]);
1291
-
1292
- return {
1293
- content: [
1294
- {
1295
- type: 'text',
1296
- text: `Successfully created test:\n\n${formattedTest}`,
1297
- },
1298
- ],
1299
- };
1300
- }
1301
-
1302
- async linkLabels(testId, labelsIds) {
1303
- if (!labelsIds || labelsIds.length === 0) {
1304
- return;
1305
- }
1306
-
1307
- // Process each label individually using the label linking API
1308
- for (const labelId of labelsIds) {
1309
- // Parse label:value format if present
1310
- let labelUid = labelId;
1311
- let value = null;
1312
-
1313
- if (labelId.includes(':')) {
1314
- [labelUid, value] = labelId.split(':', 2);
1315
- }
1316
-
1317
- // Build URL with test_id query parameter
1318
- let url = `/labels/${labelUid}/link?test_id=${testId}`;
1319
-
1320
- // Add value as query parameter if present
1321
- if (value) {
1322
- url += `&value=${encodeURIComponent(value)}`;
1323
- }
1324
-
1325
- await this.makePostRequest(url, {});
1326
- }
1327
- }
1328
-
1329
- async unlinkLabel(args) {
1330
- const { label_id, test_id, suite_id, value } = args;
1331
-
1332
- // Validate that either test_id or suite_id is provided
1333
- if (!test_id && !suite_id) {
1334
- throw new Error('Either test_id or suite_id must be provided');
1335
- }
1336
-
1337
- if (test_id && suite_id) {
1338
- throw new Error('Cannot specify both test_id and suite_id. Use one or the other.');
1339
- }
1340
-
1341
- // Build URL with base label link endpoint
1342
- let url = `/labels/${label_id}/link`;
1343
-
1344
- // Add appropriate query parameters
1345
- if (test_id) {
1346
- url += `?test_id=${test_id}`;
1347
- } else if (suite_id) {
1348
- url += `?suite_id=${suite_id}`;
1349
- }
1350
-
1351
- // Add event=remove parameter
1352
- url += `&event=remove`;
1353
-
1354
- // Add value as query parameter if present
1355
- if (value) {
1356
- url += `&value=${encodeURIComponent(value)}`;
1357
- }
1358
-
1359
- // Make the API request
1360
- await this.makePostRequest(url, {});
1361
-
1362
- const itemType = test_id ? 'test' : 'suite';
1363
- const itemId = test_id || suite_id;
1364
- const removeDescription = value ? `value "${value}"` : 'all instances';
1365
-
1366
- return {
1367
- content: [
1368
- {
1369
- type: 'text',
1370
- text: `Successfully removed label "${label_id}" (${removeDescription}) from ${itemType} "${itemId}"`,
1371
- },
1372
- ],
1373
- };
1374
- }
1375
-
1376
- async updateTest(args) {
1377
- const { test_id, suite_id, labels_ids, fields, ...attributes } = args;
1378
-
1379
- // Extract tags from title if provided
1380
- const titleTags = this.extractTagsFromTitle(attributes.title);
1381
- const mergedTags = this.mergeTags(attributes.tags, titleTags);
1382
-
1383
- // Convert attributes to use hyphens instead of underscores for API compatibility
1384
- const apiAttributes = Object.fromEntries(
1385
- Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])
1386
- );
1387
-
1388
- // Add suite_id to attributes if provided
1389
- if (suite_id) {
1390
- apiAttributes['suite_id'] = suite_id;
1391
- }
1392
-
1393
- let data;
1394
-
1395
- // Handle regular test attributes update using JSON-API format
1396
- const requestData = {
1397
- data: {
1398
- id: test_id,
1399
- type: 'tests',
1400
- attributes: {
1401
- ...apiAttributes,
1402
- ...(mergedTags.length > 0 && { tags: mergedTags }),
1403
- ...(fields && { 'custom-fields': fields })
1404
- }
1405
- }
1406
- };
1407
-
1408
- data = await this.makePutRequest(`/tests/${test_id}`, requestData);
1409
-
1410
- // Handle labels_ids using the label linking API
1411
- if (labels_ids && labels_ids.length > 0) {
1412
- await this.linkLabels(test_id, labels_ids);
1413
-
1414
- // After linking labels, fetch the updated test to reflect changes
1415
- data = await this.makeRequest(`/tests/${test_id}`);
1416
- }
1417
-
1418
- const formattedTest = this.formatModel(data.data, 'test', [
1419
- 'title', 'description', 'code', 'priority',
1420
- 'state', 'suite-id', 'tags', 'file'
1421
- ]);
1422
-
1423
- return {
1424
- content: [
1425
- {
1426
- type: 'text',
1427
- text: `Successfully updated test:\n\n${formattedTest}`,
1428
- },
1429
- ],
1430
- };
1431
- }
1432
-
1433
- async createSuite(args) {
1434
- const { parent_id, fields, ...attributes } = args;
1435
- const requestData = {
1436
- data: {
1437
- type: 'suites',
1438
- attributes: {
1439
- ...Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
1440
- 'file-type': 'file',
1441
- ...(fields && { 'custom-fields': fields })
1442
- }
1443
- }
1444
- };
1445
-
1446
- if (parent_id) {
1447
- requestData.data.relationships = {
1448
- parent: {
1449
- data: {
1450
- type: 'suites',
1451
- id: parent_id
1452
- }
1453
- }
1454
- };
1455
- }
1456
-
1457
- const data = await this.makePostRequest('/suites', requestData);
1458
- const formattedSuite = this.formatModel(data.data, 'suite', [
1459
- 'title', 'description', 'test-count', 'is-root', 'file-type'
1460
- ]);
1461
-
1462
- return {
1463
- content: [
1464
- {
1465
- type: 'text',
1466
- text: `Successfully created suite:\n\n${formattedSuite}`,
1467
- },
1468
- ],
1469
- };
1470
- }
1471
-
1472
- async createFolder(args) {
1473
- const { parent_id, fields, ...attributes } = args;
1474
- const requestData = {
1475
- data: {
1476
- type: 'suites',
1477
- attributes: {
1478
- ...Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
1479
- 'file-type': 'folder',
1480
- ...(fields && { 'custom-fields': fields })
1481
- }
1482
- }
1483
- };
1484
-
1485
- if (parent_id) {
1486
- requestData.data.relationships = {
1487
- parent: {
1488
- data: {
1489
- type: 'suites',
1490
- id: parent_id
1491
- }
1492
- }
1493
- };
1494
- }
1495
-
1496
- const data = await this.makePostRequest('/suites', requestData);
1497
- const formattedSuite = this.formatModel(data.data, 'suite', [
1498
- 'title', 'description', 'test-count', 'is-root', 'file-type'
1499
- ]);
1500
-
1501
- return {
1502
- content: [
1503
- {
1504
- type: 'text',
1505
- text: `Successfully created folder:\n\n${formattedSuite}`,
1506
- },
1507
- ],
1508
- };
1509
- }
1510
-
1511
- async createLabel(args) {
1512
- const { title, color, scope, visibility, field, ...otherAttributes } = args;
1513
-
1514
- const requestData = {
1515
- data: {
1516
- type: 'labels',
1517
- attributes: {
1518
- ...Object.fromEntries(Object.entries(otherAttributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
1519
- title,
1520
- color,
1521
- scope,
1522
- visibility
1523
- }
1524
- }
1525
- };
1526
-
1527
- // Add field configuration if provided
1528
- if (field) {
1529
- requestData.data.attributes.field = {
1530
- type: field.type,
1531
- ...(field.short !== undefined && { short: field.short }),
1532
- ...(field.value && { value: field.value })
1533
- };
1534
- }
1535
-
1536
- const data = await this.makePostRequest('/labels', requestData);
1537
- const formattedLabel = this.formatModel(data.data, 'label', [
1538
- 'title', 'color', 'scope', 'visibility', 'field'
1539
- ]);
1540
-
1541
- return {
1542
- content: [
1543
- {
1544
- type: 'text',
1545
- text: `Successfully created label:\n\n${formattedLabel}`,
1546
- },
1547
- ],
1548
- };
1549
- }
1550
-
1551
- async run() {
1552
- // Test authentication on startup
1553
- try {
1554
- await this.authenticate();
1555
- console.error('✓ Successfully authenticated with Testomatio API');
1556
- } catch (error) {
1557
- console.error('✗ Authentication failed:', error.message);
1558
- process.exit(1);
1559
- }
1560
-
1561
- const transport = new StdioServerTransport();
1562
- await this.server.connect(transport);
1563
- console.error('Testomatio MCP server running on stdio');
1564
- }
1565
- }
1566
-
1567
- // Export the class for testing
1568
- export { TestomatioMCPServer };
1569
-
1570
- // Parse command line arguments using commander
1571
- function parseArgs() {
1572
- program
1573
- .name('testomatio-mcp')
1574
- .description('Model Context Protocol server for Testomatio API')
1575
- .version('1.0.0')
1576
- .option('-t, --token <token>', 'Testomatio API token')
1577
- .option('-p, --project <project>', 'Project ID')
1578
- .option('--base-url <url>', 'Base URL for Testomatio API', 'https://app.testomat.io')
1579
- .parse();
1580
-
1581
- const options = program.opts();
1582
-
1583
- const token = normalizeString(options.token || process.env.TESTOMATIO_API_TOKEN);
1584
- const projectId = normalizeString(options.project || process.env.TESTOMATIO_PROJECT_ID);
1585
- const baseUrl = normalizeBaseUrl(
1586
- options.baseUrl ||
1587
- process.env.TESTOMATIO_BASE_URL ||
1588
- 'https://app.testomat.io'
1589
- );
1590
-
1591
- if (!token) {
1592
- console.error('Error: API token is required. Use --token <token> or set TESTOMATIO_API_TOKEN environment variable');
1593
- process.exit(1);
1594
- }
1595
-
1596
- if (!projectId) {
1597
- console.error('Error: Project ID is required. Use --project <project_id> or set TESTOMATIO_PROJECT_ID environment variable');
1598
- process.exit(1);
1599
- }
1600
-
1601
- return { token, projectId, baseUrl };
1602
- }
1603
-
1604
- // Main execution
1605
- async function main() {
1606
- try {
1607
- const config = parseArgs();
1608
- const server = new TestomatioMCPServer(config);
1609
- await server.run();
1610
- } catch (error) {
1611
- 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);
1612
20
  process.exit(1);
1613
- }
1614
- }
1615
-
1616
- // Run main() when executed directly or as a bin script
1617
- // Don't run when imported as a module for testing
1618
- if (import.meta.url.startsWith('file://') && process.argv[1]) {
1619
- const modulePath = fileURLToPath(import.meta.url);
1620
- const isDirectExecution = process.argv[1].includes('index.js') ||
1621
- process.argv[1] === modulePath ||
1622
- process.argv[1].endsWith('/testomatio-mcp');
1623
-
1624
- if (isDirectExecution) {
1625
- main().catch(console.error);
1626
- }
21
+ });
1627
22
  }