@taiga-ai/mcp-server 0.13.0-rc.424 → 0.14.0-dev.425

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.13.0-rc.424",
3
+ "version": "0.14.0-dev.425",
4
4
  "description": "MCP server for Taiga -- gives AI assistants access to documents and products",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ApiKeyIntrospection } from '@taiga/types';
1
+ import type { AgentJobType, ApiKeyIntrospection } from '@taiga/types';
2
2
  import { SERVER_VERSION } from './version.js';
3
3
 
4
4
  /**
@@ -70,6 +70,20 @@ function seg(value: string): string {
70
70
  return encodeURIComponent(value);
71
71
  }
72
72
 
73
+ /**
74
+ * A query string from the defined values only, prefixed with "?" when there
75
+ * is one. Every value goes through URLSearchParams, so nothing a tool passes
76
+ * can add a parameter of its own.
77
+ */
78
+ function query(params: Record<string, string | number | boolean | null | undefined>): string {
79
+ const search = new URLSearchParams();
80
+ for (const [key, value] of Object.entries(params)) {
81
+ if (value !== undefined && value !== null && value !== '') search.set(key, String(value));
82
+ }
83
+ const text = search.toString();
84
+ return text ? `?${text}` : '';
85
+ }
86
+
73
87
  function parseErrorBody(body: string): { code?: string; message?: string } {
74
88
  try {
75
89
  const json = JSON.parse(body) as { code?: unknown; message?: unknown; details?: unknown };
@@ -133,13 +147,15 @@ export class TaigaClient {
133
147
  'User-Agent': `taiga-ai-mcp-server/${SERVER_VERSION}`,
134
148
  };
135
149
  // Only with a body: Fastify refuses an empty body under a JSON
136
- // content type, which is what a bodiless POST (publish) would send.
137
- if (body !== undefined) headers['Content-Type'] = 'application/json';
150
+ // content type, which is what a bodiless POST (publish) would send. A
151
+ // form (a file upload) sets its own multipart type with its boundary.
152
+ const form = body instanceof FormData;
153
+ if (body !== undefined && !form) headers['Content-Type'] = 'application/json';
138
154
 
139
155
  const response = await fetch(url, {
140
156
  method,
141
157
  headers,
142
- body: body === undefined ? null : JSON.stringify(body),
158
+ body: body === undefined ? null : form ? body : JSON.stringify(body),
143
159
  // A caller that must stop waiting (a wait's deadline, a cancelled call) ends the read too.
144
160
  ...(options.signal ? { signal: options.signal } : {}),
145
161
  });
@@ -149,6 +165,8 @@ export class TaigaClient {
149
165
  throw new TaigaApiError(method, path, response.status, text);
150
166
  }
151
167
 
168
+ // A delete answers 204 with no body: there is nothing to parse.
169
+ if (response.status === 204) return null as T;
152
170
  const parsed = (await response.json()) as T;
153
171
  return options.keepTenant ? parsed : withoutTenant(parsed);
154
172
  }
@@ -313,7 +331,7 @@ export class TaigaClient {
313
331
  /** Ask an agent for work: the job is queued and runs in the background. */
314
332
  async createAgentJob(body: {
315
333
  scope: 'product' | 'tenant';
316
- type: string;
334
+ type: AgentJobType;
317
335
  targetId: string;
318
336
  documentType?: string | undefined;
319
337
  metadata?: Record<string, unknown> | undefined;
@@ -325,6 +343,44 @@ export class TaigaClient {
325
343
  return this.request<{ data: unknown }>('GET', `/agent-jobs/${seg(id)}`, undefined, { signal });
326
344
  }
327
345
 
346
+ /** The plan-side draft of a product document, or `{ data: null }` when none exists. */
347
+ async getProductDocumentDraft(productId: string, type: string) {
348
+ return this.request<{ data: TaigaDocument | null }>(
349
+ 'GET',
350
+ `/products/${seg(productId)}/documents/${seg(type)}/draft`
351
+ );
352
+ }
353
+
354
+ async saveProductDocumentDraft(productId: string, type: string, body: { data: Record<string, unknown> }) {
355
+ return this.request<{ data: TaigaDocument }>(
356
+ 'PUT',
357
+ `/products/${seg(productId)}/documents/${seg(type)}/draft`,
358
+ body
359
+ );
360
+ }
361
+
362
+ async publishProductDocument(productId: string, type: string) {
363
+ return this.request<{ data: TaigaDocument; warnings?: { unknownPolicyCitations?: string[] } }>(
364
+ 'POST',
365
+ `/products/${seg(productId)}/documents/${seg(type)}/publish`
366
+ );
367
+ }
368
+
369
+ /** Mark the published version reviewed by the caller, as the Documents page shows it. */
370
+ async markProductDocumentReviewed(productId: string, type: string) {
371
+ return this.request<{ data: unknown }>('POST', `/products/${seg(productId)}/documents/${seg(type)}/review`);
372
+ }
373
+
374
+ /** Generate remaining: every missing and outdated document, in chain order. */
375
+ async generateRemainingDocuments(productId: string) {
376
+ return this.request<{ data: unknown }>('POST', `/products/${seg(productId)}/documents/generate-all`);
377
+ }
378
+
379
+ /** Cancel a Generate remaining run still in flight. */
380
+ async stopGeneratingDocuments(productId: string) {
381
+ return this.request<{ data: unknown }>('POST', `/products/${seg(productId)}/documents/generate-all/stop`);
382
+ }
383
+
328
384
  async getProductDocument(productId: string, type: string, origin?: string) {
329
385
  const qs = origin ? `?${new URLSearchParams({ origin })}` : '';
330
386
  return this.request<{ data: unknown }>('GET', `/products/${seg(productId)}/documents/${seg(type)}${qs}`);
@@ -454,6 +510,258 @@ export class TaigaClient {
454
510
  return this.request<{ data: unknown[] }>('GET', `/builds/${seg(id)}/steps`);
455
511
  }
456
512
 
513
+ // ---------------------------------------------------------------------------
514
+ // Maintaining
515
+ // ---------------------------------------------------------------------------
516
+
517
+ async getMaintenanceSummary(productId: string) {
518
+ return this.request<{ data: unknown }>('GET', `/products/${seg(productId)}/maintenance/summary`);
519
+ }
520
+
521
+ async listMaintenanceSweeps(productId: string, opts?: { limit?: number | undefined }) {
522
+ return this.request<{ data: unknown[]; nextCursor: string | null }>(
523
+ 'GET',
524
+ `/products/${seg(productId)}/maintenance/runs${query({ limit: opts?.limit })}`
525
+ );
526
+ }
527
+
528
+ async listFindings(
529
+ productId: string,
530
+ opts?: {
531
+ status?: string | undefined;
532
+ severity?: string | undefined;
533
+ kind?: string | undefined;
534
+ limit?: number | undefined;
535
+ cursor?: string | undefined;
536
+ }
537
+ ) {
538
+ return this.request<{ data: unknown[]; nextCursor: string | null; totalCount?: number }>(
539
+ 'GET',
540
+ `/products/${seg(productId)}/maintenance/findings${query({ ...opts })}`
541
+ );
542
+ }
543
+
544
+ async getFinding(id: string) {
545
+ return this.request<{ data: unknown }>('GET', `/maintenance/findings/${seg(id)}`);
546
+ }
547
+
548
+ async updateFinding(
549
+ id: string,
550
+ body: {
551
+ status: 'open' | 'ignored';
552
+ ignoreReason?: string | undefined;
553
+ ignoreNote?: string | null | undefined;
554
+ ignoreExpiresAt?: string | null | undefined;
555
+ }
556
+ ) {
557
+ return this.request<{ data: unknown }>('PATCH', `/maintenance/findings/${seg(id)}`, body);
558
+ }
559
+
560
+ async remediateFinding(productId: string, findingId: string) {
561
+ return this.request<{ data: unknown }>(
562
+ 'POST',
563
+ `/products/${seg(productId)}/maintenance/findings/${seg(findingId)}/remediate`,
564
+ {}
565
+ );
566
+ }
567
+
568
+ async remediatePackage(
569
+ productId: string,
570
+ body: {
571
+ ecosystem: string;
572
+ packageName: string;
573
+ currentVersion?: string | null | undefined;
574
+ productRepositoryId?: string | null | undefined;
575
+ }
576
+ ) {
577
+ return this.request<{ data: unknown }>('POST', `/products/${seg(productId)}/maintenance/packages/remediate`, body);
578
+ }
579
+
580
+ async getPolicyStanding(productId: string) {
581
+ return this.request<{ data: unknown }>('GET', `/products/${seg(productId)}/maintenance/policies`);
582
+ }
583
+
584
+ async remediatePolicyControl(productId: string, controlId: string) {
585
+ return this.request<{ data: unknown }>(
586
+ 'POST',
587
+ `/products/${seg(productId)}/maintenance/policies/controls/${seg(controlId)}/remediate`,
588
+ {}
589
+ );
590
+ }
591
+
592
+ async remediatePolicyViolation(productId: string, divergenceId: string) {
593
+ return this.request<{ data: unknown }>(
594
+ 'POST',
595
+ `/products/${seg(productId)}/maintenance/policies/divergences/${seg(divergenceId)}/remediate`,
596
+ {}
597
+ );
598
+ }
599
+
600
+ // ---------------------------------------------------------------------------
601
+ // Action Required
602
+ // ---------------------------------------------------------------------------
603
+
604
+ async listInbox(opts: {
605
+ productId?: string | undefined;
606
+ category?: string | undefined;
607
+ resolved?: boolean | undefined;
608
+ limit?: number | undefined;
609
+ cursor?: string | undefined;
610
+ }) {
611
+ const { productId, ...rest } = opts;
612
+ const path = productId ? `/products/${seg(productId)}/inbox` : '/inbox';
613
+ return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `${path}${query({ ...rest })}`);
614
+ }
615
+
616
+ async updateInboxItem(
617
+ id: string,
618
+ body: { status?: 'in_progress' | 'done' | 'dismissed' | undefined; snoozedUntil?: string | null | undefined }
619
+ ) {
620
+ return this.request<{ data: unknown }>('PATCH', `/inbox/${seg(id)}`, body);
621
+ }
622
+
623
+ async listSetupTasks(opts: {
624
+ productId?: string | undefined;
625
+ limit?: number | undefined;
626
+ cursor?: string | undefined;
627
+ }) {
628
+ const { productId, ...rest } = opts;
629
+ const path = productId ? `/products/${seg(productId)}/setup-tasks` : '/setup-tasks';
630
+ return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `${path}${query({ ...rest })}`);
631
+ }
632
+
633
+ // ---------------------------------------------------------------------------
634
+ // Deployments, environments and monitoring (observed, never driven)
635
+ // ---------------------------------------------------------------------------
636
+
637
+ async listEnvironments(productId: string, opts?: { limit?: number | undefined; cursor?: string | undefined }) {
638
+ return this.request<{ data: unknown[]; nextCursor: string | null }>(
639
+ 'GET',
640
+ `/products/${seg(productId)}/environments${query({ ...opts })}`
641
+ );
642
+ }
643
+
644
+ async createEnvironment(productId: string, body: { name: string; type: string }) {
645
+ return this.request<{ data: unknown }>('POST', `/products/${seg(productId)}/environments`, body);
646
+ }
647
+
648
+ async updateEnvironment(
649
+ id: string,
650
+ body: { name?: string | undefined; type?: string | undefined; url?: string | null | undefined }
651
+ ) {
652
+ return this.request<{ data: unknown }>('PATCH', `/environments/${seg(id)}`, body);
653
+ }
654
+
655
+ async getCurrentDeployments(productId: string) {
656
+ return this.request<{ data: unknown }>('GET', `/products/${seg(productId)}/deployments/current`);
657
+ }
658
+
659
+ async listDeployments(
660
+ productId: string,
661
+ opts?: { environmentId?: string | undefined; limit?: number | undefined; cursor?: string | undefined }
662
+ ) {
663
+ return this.request<{ data: unknown[]; nextCursor: string | null }>(
664
+ 'GET',
665
+ `/products/${seg(productId)}/deployments${query({ ...opts })}`
666
+ );
667
+ }
668
+
669
+ async getDeploymentSyncStatus(productId: string) {
670
+ return this.request<{ data: unknown }>('GET', `/products/${seg(productId)}/deployments/sync-status`);
671
+ }
672
+
673
+ async getDeliveryMetrics(productId: string, environmentId: string, windowDays: number) {
674
+ return this.request<{ data: unknown }>(
675
+ 'GET',
676
+ `/products/${seg(productId)}/deployments/insights${query({ environmentId, windowDays })}`
677
+ );
678
+ }
679
+
680
+ async getMonitoringHealth(productId: string) {
681
+ return this.request<{ data: unknown[] }>('GET', `/products/${seg(productId)}/monitoring/health`);
682
+ }
683
+
684
+ async getMonitoringSignal(
685
+ productId: string,
686
+ signal: 'uptime' | 'rum' | 'client-health' | 'history',
687
+ environmentId: string,
688
+ windowDays: number
689
+ ) {
690
+ return this.request<{ data: unknown }>(
691
+ 'GET',
692
+ `/products/${seg(productId)}/monitoring/${signal}${query({ environmentId, windowDays })}`
693
+ );
694
+ }
695
+
696
+ // ---------------------------------------------------------------------------
697
+ // Knowledge
698
+ // ---------------------------------------------------------------------------
699
+
700
+ async listKnowledge(opts: {
701
+ factoryId?: string | null | undefined;
702
+ productId?: string | null | undefined;
703
+ limit?: number | undefined;
704
+ cursor?: string | undefined;
705
+ }) {
706
+ // The route reads the literal 'null' as "this level only, not below it".
707
+ const scope = (value: string | null | undefined) => (value === null ? 'null' : value);
708
+ return this.request<{ data: unknown[]; nextCursor: string | null }>(
709
+ 'GET',
710
+ `/custom-documents${query({ ...opts, factoryId: scope(opts.factoryId), productId: scope(opts.productId) })}`
711
+ );
712
+ }
713
+
714
+ async getKnowledge(id: string) {
715
+ return this.request<{ data: unknown }>('GET', `/custom-documents/${seg(id)}`);
716
+ }
717
+
718
+ async getKnowledgeDownload(id: string) {
719
+ return this.request<{ data: { url?: unknown } & Record<string, unknown> }>(
720
+ 'GET',
721
+ `/custom-documents/${seg(id)}/download`
722
+ );
723
+ }
724
+
725
+ /** A text file filed as knowledge at one level: the organization, a factory, or a product in its factory. */
726
+ async addKnowledge(file: {
727
+ fileName: string;
728
+ mime: 'text/markdown' | 'text/plain';
729
+ content: string;
730
+ factoryId?: string | undefined;
731
+ productId?: string | undefined;
732
+ }) {
733
+ const form = new FormData();
734
+ // Fields before the file: the route reads them in any order, but the
735
+ // order a person would read them in costs nothing.
736
+ if (file.factoryId) form.set('factoryId', file.factoryId);
737
+ if (file.productId) form.set('productId', file.productId);
738
+ form.set('file', new Blob([file.content], { type: file.mime }), file.fileName);
739
+ return this.request<{ data: unknown; deduplicated?: boolean }>('POST', '/custom-documents', form);
740
+ }
741
+
742
+ async retryKnowledge(id: string) {
743
+ return this.request<{ data: unknown }>('POST', `/custom-documents/${seg(id)}/retry`);
744
+ }
745
+
746
+ async deleteKnowledge(id: string) {
747
+ return this.request<{ data?: unknown } | null>('DELETE', `/custom-documents/${seg(id)}`);
748
+ }
749
+
750
+ // ---------------------------------------------------------------------------
751
+ // Autonomy
752
+ // ---------------------------------------------------------------------------
753
+
754
+ async updateProduct(
755
+ productId: string,
756
+ body: { autoMerge?: boolean | null | undefined; autoBuild?: boolean | undefined }
757
+ ) {
758
+ return this.request<{ data: unknown }>('PATCH', `/products/${seg(productId)}`, body);
759
+ }
760
+
761
+ async updateBacklogItem(itemId: string, body: { autoMerge?: boolean | null | undefined }) {
762
+ return this.request<{ data: unknown }>('PATCH', `/backlog/${seg(itemId)}`, body);
763
+ }
764
+
457
765
  // ---------------------------------------------------------------------------
458
766
  // Factories
459
767
  // ---------------------------------------------------------------------------