@taiga-ai/mcp-server 0.4.0-dev.383 → 0.4.0-dev.387

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/dist/index.js CHANGED
@@ -28,7 +28,7 @@ var TaigaClient = class {
28
28
  const response = await fetch(url, {
29
29
  method,
30
30
  headers,
31
- body: body ? JSON.stringify(body) : void 0
31
+ body: body ? JSON.stringify(body) : null
32
32
  });
33
33
  if (!response.ok) {
34
34
  const text = await response.text().catch(() => "");
@@ -167,6 +167,21 @@ var TaigaClient = class {
167
167
  }
168
168
  };
169
169
  //#endregion
170
+ //#region src/config.ts
171
+ const envSchema = z.object({
172
+ TAIGA_API_KEY: z.string().min(1),
173
+ TAIGA_API_URL: z.string().default("https://api.taiga.app")
174
+ });
175
+ /** Null when TAIGA_API_KEY is missing or empty: it is the one setting with no default. */
176
+ function readConfig() {
177
+ const parsed = envSchema.safeParse(process.env);
178
+ if (!parsed.success) return null;
179
+ return {
180
+ apiKey: parsed.data.TAIGA_API_KEY,
181
+ apiUrl: parsed.data.TAIGA_API_URL
182
+ };
183
+ }
184
+ //#endregion
170
185
  //#region src/tools/summarize.ts
171
186
  /**
172
187
  * Strip heavy content from list responses to avoid context window bloat.
@@ -357,17 +372,16 @@ function registerAllTools(server, client) {
357
372
  }
358
373
  //#endregion
359
374
  //#region src/index.ts
360
- const DEFAULT_API_URL = "https://api.taiga.app";
361
375
  async function main() {
362
- const apiKey = process.env.TAIGA_API_KEY;
363
- if (!apiKey) {
376
+ const config = readConfig();
377
+ if (!config) {
364
378
  console.error("Error: TAIGA_API_KEY environment variable is required.");
365
379
  console.error("Create an API key at https://app.taiga.app/settings/api-keys");
366
380
  process.exit(1);
367
381
  }
368
382
  const client = new TaigaClient({
369
- baseUrl: process.env.TAIGA_API_URL ?? DEFAULT_API_URL,
370
- apiKey
383
+ baseUrl: config.apiUrl,
384
+ apiKey: config.apiKey
371
385
  });
372
386
  const introspection = await client.introspect().catch((error) => {
373
387
  const message = error instanceof Error ? error.message : String(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.4.0-dev.383",
3
+ "version": "0.4.0-dev.387",
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",
@@ -26,6 +26,7 @@
26
26
  "zod": "^4.4.3"
27
27
  },
28
28
  "devDependencies": {
29
+ "@tsconfig/strictest": "^2.0.8",
29
30
  "@types/node": "^22.20.1",
30
31
  "rolldown": "^1.2.3",
31
32
  "typescript": "^7.0.2",
@@ -57,7 +57,7 @@ describe('TaigaClient', () => {
57
57
  await client.listProducts();
58
58
 
59
59
  // Assert
60
- const headers = spy.mock.calls[0][1].headers;
60
+ const headers = spy.mock.calls[0]![1].headers;
61
61
  expect(headers.Authorization).toBe('Bearer taiga_sk_live_secret42');
62
62
  });
63
63
 
@@ -66,7 +66,7 @@ describe('TaigaClient', () => {
66
66
 
67
67
  await makeClient().listProducts();
68
68
 
69
- expect(spy.mock.calls[0][1].headers['User-Agent']).toBe(`taiga-ai-mcp-server/${pkg.version}`);
69
+ expect(spy.mock.calls[0]![1].headers['User-Agent']).toBe(`taiga-ai-mcp-server/${pkg.version}`);
70
70
  });
71
71
  });
72
72
 
@@ -113,7 +113,7 @@ describe('TaigaClient', () => {
113
113
 
114
114
  await client.listProducts();
115
115
 
116
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products');
116
+ expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/products');
117
117
  });
118
118
 
119
119
  it('should omit query string when no optional params are provided', async () => {
@@ -121,7 +121,7 @@ describe('TaigaClient', () => {
121
121
 
122
122
  await makeClient().listProducts();
123
123
 
124
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products');
124
+ expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/products');
125
125
  });
126
126
 
127
127
  it('should include all optional filters as query params', async () => {
@@ -129,7 +129,7 @@ describe('TaigaClient', () => {
129
129
 
130
130
  await makeClient().listProducts({ factoryId: 't1', status: 'active', limit: 10 });
131
131
 
132
- const url = spy.mock.calls[0][0] as string;
132
+ const url = spy.mock.calls[0]![0] as string;
133
133
  expect(url).toContain('factoryId=t1');
134
134
  expect(url).toContain('status=active');
135
135
  expect(url).toContain('limit=10');
@@ -140,7 +140,7 @@ describe('TaigaClient', () => {
140
140
 
141
141
  await makeClient().listBacklogItems('proj-1', { limit: 20 });
142
142
 
143
- const url = spy.mock.calls[0][0] as string;
143
+ const url = spy.mock.calls[0]![0] as string;
144
144
  expect(url).toContain('/v1/products/proj-1/backlog');
145
145
  expect(url).toContain('limit=20');
146
146
  });
@@ -150,7 +150,7 @@ describe('TaigaClient', () => {
150
150
 
151
151
  await makeClient().listEnvironments('proj-1');
152
152
 
153
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products/proj-1/environments');
153
+ expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/products/proj-1/environments');
154
154
  });
155
155
  });
156
156
 
@@ -211,7 +211,7 @@ describe('TaigaClient', () => {
211
211
 
212
212
  const result = await makeClient().getProduct('p1');
213
213
 
214
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products/p1');
214
+ expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/products/p1');
215
215
  expect(result.data).toEqual({ id: 'p1', name: 'Alpha' });
216
216
  });
217
217
 
@@ -220,7 +220,7 @@ describe('TaigaClient', () => {
220
220
 
221
221
  await makeClient().getTenantDocument('security_policy');
222
222
 
223
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/tenants/current/documents/security_policy');
223
+ expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/tenants/current/documents/security_policy');
224
224
  });
225
225
 
226
226
  it('should PUT a document draft with the provided body', async () => {
@@ -228,9 +228,9 @@ describe('TaigaClient', () => {
228
228
 
229
229
  await makeClient().saveTenantDocumentDraft('security_policy', { data: { title: 'New' } });
230
230
 
231
- expect(spy.mock.calls[0][1].method).toBe('PUT');
232
- expect(spy.mock.calls[0][0]).toContain('/tenants/current/documents/security_policy/draft');
233
- expect(JSON.parse(spy.mock.calls[0][1].body)).toEqual({
231
+ expect(spy.mock.calls[0]![1].method).toBe('PUT');
232
+ expect(spy.mock.calls[0]![0]).toContain('/tenants/current/documents/security_policy/draft');
233
+ expect(JSON.parse(spy.mock.calls[0]![1].body)).toEqual({
234
234
  data: { title: 'New' },
235
235
  });
236
236
  });
@@ -240,7 +240,7 @@ describe('TaigaClient', () => {
240
240
 
241
241
  await makeClient().getBacklogItem('item-1');
242
242
 
243
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/backlog/item-1');
243
+ expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/backlog/item-1');
244
244
  });
245
245
 
246
246
  it('should GET factories with optional limit', async () => {
@@ -248,7 +248,7 @@ describe('TaigaClient', () => {
248
248
 
249
249
  await makeClient().listFactories({ limit: 50 });
250
250
 
251
- expect(spy.mock.calls[0][0]).toContain('/v1/factories?limit=50');
251
+ expect(spy.mock.calls[0]![0]).toContain('/v1/factories?limit=50');
252
252
  });
253
253
 
254
254
  it('should GET documents from tenant path with optional limit', async () => {
@@ -256,7 +256,7 @@ describe('TaigaClient', () => {
256
256
 
257
257
  await makeClient().listTenantDocuments({ limit: 5 });
258
258
 
259
- expect(spy.mock.calls[0][0]).toContain('/v1/tenants/current/documents?limit=5');
259
+ expect(spy.mock.calls[0]![0]).toContain('/v1/tenants/current/documents?limit=5');
260
260
  });
261
261
  });
262
262
  });
package/src/client.ts CHANGED
@@ -26,7 +26,7 @@ export class TaigaClient {
26
26
  const response = await fetch(url, {
27
27
  method,
28
28
  headers,
29
- body: body ? JSON.stringify(body) : undefined,
29
+ body: body ? JSON.stringify(body) : null,
30
30
  });
31
31
 
32
32
  if (!response.ok) {
@@ -60,7 +60,11 @@ export class TaigaClient {
60
60
  // Products
61
61
  // ---------------------------------------------------------------------------
62
62
 
63
- async listProducts(opts?: { factoryId?: string; status?: string; limit?: number }) {
63
+ async listProducts(opts?: {
64
+ factoryId?: string | undefined;
65
+ status?: string | undefined;
66
+ limit?: number | undefined;
67
+ }) {
64
68
  const params = new URLSearchParams();
65
69
  if (opts?.factoryId) params.set('factoryId', opts.factoryId);
66
70
  if (opts?.status) params.set('status', opts.status);
@@ -77,7 +81,7 @@ export class TaigaClient {
77
81
  // Documents (tenant-level policies)
78
82
  // ---------------------------------------------------------------------------
79
83
 
80
- async listTenantDocuments(opts?: { limit?: number }) {
84
+ async listTenantDocuments(opts?: { limit?: number | undefined }) {
81
85
  const params = new URLSearchParams();
82
86
  if (opts?.limit) params.set('limit', String(opts.limit));
83
87
  const qs = params.toString();
@@ -99,7 +103,7 @@ export class TaigaClient {
99
103
  // Factory Documents
100
104
  // ---------------------------------------------------------------------------
101
105
 
102
- async listFactoryDocuments(factoryId: string, opts?: { limit?: number }) {
106
+ async listFactoryDocuments(factoryId: string, opts?: { limit?: number | undefined }) {
103
107
  const params = new URLSearchParams();
104
108
  if (opts?.limit) params.set('limit', String(opts.limit));
105
109
  const qs = params.toString();
@@ -121,7 +125,7 @@ export class TaigaClient {
121
125
  // Product Documents
122
126
  // ---------------------------------------------------------------------------
123
127
 
124
- async listProductDocuments(productId: string, opts?: { limit?: number }) {
128
+ async listProductDocuments(productId: string, opts?: { limit?: number | undefined }) {
125
129
  const params = new URLSearchParams();
126
130
  if (opts?.limit) params.set('limit', String(opts.limit));
127
131
  const qs = params.toString();
@@ -214,7 +218,7 @@ export class TaigaClient {
214
218
  // Backlog
215
219
  // ---------------------------------------------------------------------------
216
220
 
217
- async listBacklogItems(productId: string, opts?: { limit?: number }) {
221
+ async listBacklogItems(productId: string, opts?: { limit?: number | undefined }) {
218
222
  const params = new URLSearchParams();
219
223
  if (opts?.limit) params.set('limit', String(opts.limit));
220
224
  const qs = params.toString();
@@ -240,7 +244,7 @@ export class TaigaClient {
240
244
  // Factories
241
245
  // ---------------------------------------------------------------------------
242
246
 
243
- async listFactories(opts?: { limit?: number }) {
247
+ async listFactories(opts?: { limit?: number | undefined }) {
244
248
  const params = new URLSearchParams();
245
249
  if (opts?.limit) params.set('limit', String(opts.limit));
246
250
  const qs = params.toString();
@@ -0,0 +1,24 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { readConfig } from './config.js';
4
+
5
+ afterEach(() => {
6
+ vi.unstubAllEnvs();
7
+ });
8
+
9
+ describe('readConfig', () => {
10
+ it('needs an API key', () => {
11
+ vi.stubEnv('TAIGA_API_KEY', undefined);
12
+ expect(readConfig()).toBeNull();
13
+ vi.stubEnv('TAIGA_API_KEY', '');
14
+ expect(readConfig()).toBeNull();
15
+ });
16
+
17
+ it('defaults the API URL', () => {
18
+ vi.stubEnv('TAIGA_API_KEY', 'taiga_sk_live_test');
19
+ vi.stubEnv('TAIGA_API_URL', undefined);
20
+ expect(readConfig()).toEqual({ apiKey: 'taiga_sk_live_test', apiUrl: 'https://api.taiga.app' });
21
+ vi.stubEnv('TAIGA_API_URL', 'http://localhost:3001');
22
+ expect(readConfig()?.apiUrl).toBe('http://localhost:3001');
23
+ });
24
+ });
package/src/config.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * The one place the MCP server reads its environment (PRO-2511), parsed once
5
+ * at startup.
6
+ */
7
+
8
+ const DEFAULT_API_URL = 'https://api.taiga.app';
9
+
10
+ const envSchema = z.object({
11
+ TAIGA_API_KEY: z.string().min(1),
12
+ TAIGA_API_URL: z.string().default(DEFAULT_API_URL),
13
+ });
14
+
15
+ export interface ServerConfig {
16
+ readonly apiKey: string;
17
+ readonly apiUrl: string;
18
+ }
19
+
20
+ /** Null when TAIGA_API_KEY is missing or empty: it is the one setting with no default. */
21
+ export function readConfig(): ServerConfig | null {
22
+ const parsed = envSchema.safeParse(process.env);
23
+ if (!parsed.success) return null;
24
+ return { apiKey: parsed.data.TAIGA_API_KEY, apiUrl: parsed.data.TAIGA_API_URL };
25
+ }
package/src/index.ts CHANGED
@@ -3,22 +3,19 @@
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
5
  import { TaigaClient } from './client.js';
6
+ import { readConfig } from './config.js';
6
7
  import { registerAllTools } from './tools/index.js';
7
8
  import { SERVER_VERSION } from './version.js';
8
9
 
9
- const DEFAULT_API_URL = 'https://api.taiga.app';
10
-
11
10
  async function main() {
12
- const apiKey = process.env.TAIGA_API_KEY;
13
- if (!apiKey) {
11
+ const config = readConfig();
12
+ if (!config) {
14
13
  console.error('Error: TAIGA_API_KEY environment variable is required.');
15
14
  console.error('Create an API key at https://app.taiga.app/settings/api-keys');
16
15
  process.exit(1);
17
16
  }
18
17
 
19
- const apiUrl = process.env.TAIGA_API_URL ?? DEFAULT_API_URL;
20
-
21
- const client = new TaigaClient({ baseUrl: apiUrl, apiKey });
18
+ const client = new TaigaClient({ baseUrl: config.apiUrl, apiKey: config.apiKey });
22
19
 
23
20
  // Introspect the API key to discover available capabilities
24
21
  const introspection = await client.introspect().catch((error) => {
@@ -110,7 +110,7 @@ function toolNames(tools: { name: string }[]) {
110
110
  /** Extract parsed JSON from the first text content block of a tool call result. */
111
111
  function parseToolResult(result: Record<string, unknown>) {
112
112
  const content = result.content as { type: string; text: string }[];
113
- return JSON.parse(content[0].text);
113
+ return JSON.parse(content[0]!.text);
114
114
  }
115
115
 
116
116
  // ---------------------------------------------------------------------------
package/tsconfig.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
+ "extends": "@tsconfig/strictest/tsconfig.json",
2
3
  "compilerOptions": {
3
4
  "target": "ES2022",
4
5
  "module": "ESNext",
5
6
  "moduleResolution": "bundler",
6
7
  "declaration": true,
7
8
  "declarationMap": true,
8
- "strict": true,
9
- "skipLibCheck": true,
10
9
  "outDir": "./dist",
11
10
  "rootDir": "./src",
12
- "esModuleInterop": true,
13
11
  "resolveJsonModule": true,
14
- "types": ["node"]
12
+ "types": ["node"],
13
+
14
+ // Preset flags this package still fails (PRO-2509). Each line is deleted
15
+ // by the issue named beside it; no new line is added here.
16
+ "noPropertyAccessFromIndexSignature": false // PRO-2519
15
17
  },
16
18
  "include": ["src"],
17
19
  "exclude": ["node_modules", "dist"],