@taiga-ai/mcp-server 0.2.3-dev.267 → 0.2.3-dev.59

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.
Files changed (40) hide show
  1. package/dist/index.js +495 -62
  2. package/package.json +8 -6
  3. package/scripts/build.mjs +37 -0
  4. package/scripts/prepare-publish.cjs +15 -7
  5. package/src/client.ts +13 -5
  6. package/src/index.ts +19 -7
  7. package/src/server.integration.test.ts +99 -45
  8. package/src/tools/documents.ts +1 -1
  9. package/src/tools/index.ts +5 -1
  10. package/taiga-ai-mcp-server-0.2.3-dev.59.tgz +0 -0
  11. package/dist/client.d.ts +0 -109
  12. package/dist/client.d.ts.map +0 -1
  13. package/dist/client.js +0 -228
  14. package/dist/client.test.d.ts +0 -2
  15. package/dist/client.test.d.ts.map +0 -1
  16. package/dist/client.test.js +0 -203
  17. package/dist/index.d.ts +0 -3
  18. package/dist/index.d.ts.map +0 -1
  19. package/dist/server.integration.test.d.ts +0 -2
  20. package/dist/server.integration.test.d.ts.map +0 -1
  21. package/dist/server.integration.test.js +0 -358
  22. package/dist/tools/backlogs.d.ts +0 -4
  23. package/dist/tools/backlogs.d.ts.map +0 -1
  24. package/dist/tools/backlogs.js +0 -6
  25. package/dist/tools/documents.d.ts +0 -4
  26. package/dist/tools/documents.d.ts.map +0 -1
  27. package/dist/tools/documents.js +0 -97
  28. package/dist/tools/index.d.ts +0 -12
  29. package/dist/tools/index.d.ts.map +0 -1
  30. package/dist/tools/index.js +0 -18
  31. package/dist/tools/projects.d.ts +0 -4
  32. package/dist/tools/projects.d.ts.map +0 -1
  33. package/dist/tools/projects.js +0 -64
  34. package/dist/tools/skills.d.ts +0 -4
  35. package/dist/tools/skills.d.ts.map +0 -1
  36. package/dist/tools/skills.js +0 -45
  37. package/dist/tools/summarize.d.ts +0 -15
  38. package/dist/tools/summarize.d.ts.map +0 -1
  39. package/dist/tools/summarize.js +0 -26
  40. package/taiga-ai-mcp-server-0.2.3-dev.267.tgz +0 -0
package/dist/client.js DELETED
@@ -1,228 +0,0 @@
1
- /**
2
- * HTTP client for the Taiga API.
3
- * Used by the MCP server to proxy tool calls to the backend.
4
- */
5
- export class TaigaClient {
6
- baseUrl;
7
- apiKey;
8
- introspection = null;
9
- constructor(opts) {
10
- this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
11
- this.apiKey = opts.apiKey;
12
- }
13
- async request(method, path, body) {
14
- const url = `${this.baseUrl}/v1${path}`;
15
- const headers = {
16
- Authorization: `Bearer ${this.apiKey}`,
17
- 'Content-Type': 'application/json',
18
- 'User-Agent': 'taiga-ai-mcp-server/0.0.1',
19
- };
20
- const response = await fetch(url, {
21
- method,
22
- headers,
23
- body: body ? JSON.stringify(body) : undefined,
24
- });
25
- if (!response.ok) {
26
- const text = await response.text().catch(() => '');
27
- throw new Error(`Taiga API ${method} ${path} failed (${response.status}): ${text}`);
28
- }
29
- return response.json();
30
- }
31
- // ---------------------------------------------------------------------------
32
- // Auth
33
- // ---------------------------------------------------------------------------
34
- async introspect() {
35
- const result = await this.request('GET', '/auth/introspect');
36
- this.introspection = result.data;
37
- return result.data;
38
- }
39
- getIntrospection() {
40
- return this.introspection;
41
- }
42
- hasCapability(capability) {
43
- if (!this.introspection)
44
- return false;
45
- return this.introspection.capabilities.includes(capability);
46
- }
47
- // ---------------------------------------------------------------------------
48
- // Projects
49
- // ---------------------------------------------------------------------------
50
- async listProjects(opts) {
51
- const params = new URLSearchParams();
52
- if (opts?.teamId)
53
- params.set('teamId', opts.teamId);
54
- if (opts?.status)
55
- params.set('status', opts.status);
56
- if (opts?.limit)
57
- params.set('limit', String(opts.limit));
58
- const qs = params.toString();
59
- return this.request('GET', `/projects${qs ? `?${qs}` : ''}`);
60
- }
61
- async getProject(id) {
62
- return this.request('GET', `/projects/${id}`);
63
- }
64
- // ---------------------------------------------------------------------------
65
- // Documents (tenant-level policies)
66
- // ---------------------------------------------------------------------------
67
- async listTenantDocuments(opts) {
68
- const params = new URLSearchParams();
69
- if (opts?.limit)
70
- params.set('limit', String(opts.limit));
71
- const qs = params.toString();
72
- return this.request('GET', `/tenants/current/documents${qs ? `?${qs}` : ''}`);
73
- }
74
- async getTenantDocument(type) {
75
- return this.request('GET', `/tenants/current/documents/${type}`);
76
- }
77
- async saveTenantDocumentDraft(type, body) {
78
- return this.request('PUT', `/tenants/current/documents/${type}/draft`, body);
79
- }
80
- // ---------------------------------------------------------------------------
81
- // Team Documents
82
- // ---------------------------------------------------------------------------
83
- async listTeamDocuments(teamId, opts) {
84
- const params = new URLSearchParams();
85
- if (opts?.limit)
86
- params.set('limit', String(opts.limit));
87
- const qs = params.toString();
88
- return this.request('GET', `/teams/${teamId}/documents${qs ? `?${qs}` : ''}`);
89
- }
90
- async getTeamDocument(teamId, type) {
91
- return this.request('GET', `/teams/${teamId}/documents/${type}`);
92
- }
93
- async saveTeamDocumentDraft(teamId, type, body) {
94
- return this.request('PUT', `/teams/${teamId}/documents/${type}/draft`, body);
95
- }
96
- // ---------------------------------------------------------------------------
97
- // Project Documents
98
- // ---------------------------------------------------------------------------
99
- async listProjectDocuments(projectId, opts) {
100
- const params = new URLSearchParams();
101
- if (opts?.limit)
102
- params.set('limit', String(opts.limit));
103
- const qs = params.toString();
104
- return this.request('GET', `/projects/${projectId}/documents${qs ? `?${qs}` : ''}`);
105
- }
106
- async getProjectDocument(projectId, type) {
107
- return this.request('GET', `/projects/${projectId}/documents/${type}`);
108
- }
109
- // ---------------------------------------------------------------------------
110
- // Skills (agent skills)
111
- // ---------------------------------------------------------------------------
112
- async listSkills(opts) {
113
- const params = new URLSearchParams();
114
- if (opts?.scope)
115
- params.set('scope', opts.scope);
116
- if (opts?.scopeId)
117
- params.set('scopeId', opts.scopeId);
118
- if (opts?.limit)
119
- params.set('limit', String(opts.limit));
120
- const qs = params.toString();
121
- return this.request('GET', `/agent-skills${qs ? `?${qs}` : ''}`);
122
- }
123
- async getSkill(id) {
124
- return this.request('GET', `/agent-skills/${id}`);
125
- }
126
- async updateSkill(id, body) {
127
- return this.request('PATCH', `/agent-skills/${id}`, body);
128
- }
129
- // ---------------------------------------------------------------------------
130
- // Team Documents
131
- // ---------------------------------------------------------------------------
132
- /**
133
- * Fetch the full context for a project in parallel: project metadata,
134
- * team documents, project documents, and team skills.
135
- * Returns a structured bundle optimized for coding agent consumption.
136
- */
137
- async getProjectContext(projectId) {
138
- // First fetch the project to get the teamId
139
- const project = await this.request('GET', `/projects/${projectId}`);
140
- const teamId = project.data.teamId;
141
- // Fetch everything in parallel
142
- const [projectDocs, teamDocs, skills] = await Promise.all([
143
- this.listProjectDocuments(projectId, { limit: 20 }).catch(() => ({ data: [] })),
144
- teamId
145
- ? this.listTeamDocuments(teamId, { limit: 20 }).catch(() => ({ data: [] }))
146
- : Promise.resolve({ data: [] }),
147
- this.listSkills({ limit: 100 }).catch(() => ({ data: [] })),
148
- ]);
149
- // Fetch full content for key documents (in parallel)
150
- const keyProjectDocTypes = ['specification', 'architecture', 'technology_decisions'];
151
- const keyTeamDocTypes = ['domain_context', 'tech_preferences', 'architecture_constraints'];
152
- const docFetches = [];
153
- for (const docType of keyProjectDocTypes) {
154
- const exists = projectDocs.data.some((d) => d.type === docType && !d.isDraft);
155
- if (exists) {
156
- docFetches.push(this.getProjectDocument(projectId, docType)
157
- .then((r) => ({ type: docType, scope: 'project', data: r.data }))
158
- .catch(() => ({ type: docType, scope: 'project', data: null })));
159
- }
160
- }
161
- if (teamId) {
162
- for (const docType of keyTeamDocTypes) {
163
- const exists = teamDocs.data.some((d) => d.type === docType && !d.isDraft);
164
- if (exists) {
165
- docFetches.push(this.getTeamDocument(teamId, docType)
166
- .then((r) => ({ type: docType, scope: 'team', data: r.data }))
167
- .catch(() => ({ type: docType, scope: 'team', data: null })));
168
- }
169
- }
170
- }
171
- const fetchedDocs = await Promise.all(docFetches);
172
- // Assemble context bundle
173
- const context = {
174
- project: project.data,
175
- documents: {
176
- project: Object.fromEntries(fetchedDocs.filter((d) => d.scope === 'project' && d.data).map((d) => [d.type, d.data])),
177
- team: Object.fromEntries(fetchedDocs.filter((d) => d.scope === 'team' && d.data).map((d) => [d.type, d.data])),
178
- },
179
- available_documents: {
180
- project: projectDocs.data.map((d) => ({
181
- type: d.type,
182
- name: d.name,
183
- version: d.version,
184
- isDraft: d.isDraft,
185
- })),
186
- team: teamDocs.data.map((d) => ({
187
- type: d.type,
188
- name: d.name,
189
- version: d.version,
190
- isDraft: d.isDraft,
191
- })),
192
- },
193
- skills_summary: skills.data
194
- .filter((s) => !s.teamId || s.teamId === teamId)
195
- .map((s) => ({ id: s.id, name: s.name, slug: s.slug, description: s.description })),
196
- };
197
- return context;
198
- }
199
- // ---------------------------------------------------------------------------
200
- // Backlog
201
- // ---------------------------------------------------------------------------
202
- async listBacklogItems(projectId, opts) {
203
- const params = new URLSearchParams();
204
- if (opts?.limit)
205
- params.set('limit', String(opts.limit));
206
- const qs = params.toString();
207
- return this.request('GET', `/projects/${projectId}/backlog${qs ? `?${qs}` : ''}`);
208
- }
209
- async getBacklogItem(itemId) {
210
- return this.request('GET', `/backlog/${itemId}`);
211
- }
212
- // ---------------------------------------------------------------------------
213
- // Environments
214
- // ---------------------------------------------------------------------------
215
- async listEnvironments(projectId) {
216
- return this.request('GET', `/projects/${projectId}/environments`);
217
- }
218
- // ---------------------------------------------------------------------------
219
- // Teams
220
- // ---------------------------------------------------------------------------
221
- async listTeams(opts) {
222
- const params = new URLSearchParams();
223
- if (opts?.limit)
224
- params.set('limit', String(opts.limit));
225
- const qs = params.toString();
226
- return this.request('GET', `/teams${qs ? `?${qs}` : ''}`);
227
- }
228
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=client.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../src/client.test.ts"],"names":[],"mappings":""}
@@ -1,203 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { TaigaClient } from './client.js';
3
- // ---------------------------------------------------------------------------
4
- // Helpers -- thin wrappers to reduce boilerplate, not to obscure intent
5
- // ---------------------------------------------------------------------------
6
- function stubFetch(status, body) {
7
- const spy = vi.fn().mockResolvedValue({
8
- ok: status >= 200 && status < 300,
9
- status,
10
- json: () => Promise.resolve(body),
11
- text: () => Promise.resolve(JSON.stringify(body)),
12
- });
13
- vi.stubGlobal('fetch', spy);
14
- return spy;
15
- }
16
- function makeClient(opts) {
17
- return new TaigaClient({
18
- baseUrl: opts?.baseUrl ?? 'https://api.test.com',
19
- apiKey: opts?.apiKey ?? 'taiga_sk_live_testkey123',
20
- });
21
- }
22
- async function clientWithCapabilities(caps) {
23
- stubFetch(200, { data: { capabilities: caps } });
24
- const client = makeClient();
25
- await client.introspect();
26
- return client;
27
- }
28
- // ---------------------------------------------------------------------------
29
- // Tests
30
- // ---------------------------------------------------------------------------
31
- describe('TaigaClient', () => {
32
- beforeEach(() => {
33
- vi.restoreAllMocks();
34
- });
35
- // -------------------------------------------------------------------------
36
- // Authentication -- the client must always identify itself correctly
37
- // -------------------------------------------------------------------------
38
- describe('authentication', () => {
39
- it('should authenticate every request with the provided API key', async () => {
40
- // Arrange
41
- const spy = stubFetch(200, { data: [] });
42
- const client = makeClient({ apiKey: 'taiga_sk_live_secret42' });
43
- // Act
44
- await client.listProjects();
45
- // Assert
46
- const headers = spy.mock.calls[0][1].headers;
47
- expect(headers.Authorization).toBe('Bearer taiga_sk_live_secret42');
48
- });
49
- it('should identify itself as taiga-ai-mcp-server in every request', async () => {
50
- const spy = stubFetch(200, { data: [] });
51
- await makeClient().listProjects();
52
- expect(spy.mock.calls[0][1].headers['User-Agent']).toMatch(/^taiga-ai-mcp-server\//);
53
- });
54
- });
55
- // -------------------------------------------------------------------------
56
- // Error handling -- failures must surface clearly, never silently swallow
57
- // -------------------------------------------------------------------------
58
- describe('error handling', () => {
59
- it('should reject with status code and body when API returns an error', async () => {
60
- stubFetch(403, { error: 'Forbidden' });
61
- const client = makeClient();
62
- await expect(client.listProjects()).rejects.toThrow(/403.*Forbidden/);
63
- });
64
- it('should reject with status code even when response body is unreadable', async () => {
65
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
66
- ok: false,
67
- status: 500,
68
- text: () => Promise.reject(new Error('stream error')),
69
- }));
70
- await expect(makeClient().listProjects()).rejects.toThrow(/500/);
71
- });
72
- it('should reject when fetch itself throws (network failure)', async () => {
73
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
74
- await expect(makeClient().listProjects()).rejects.toThrow(/ECONNREFUSED/);
75
- });
76
- });
77
- // -------------------------------------------------------------------------
78
- // URL construction -- trailing slashes, optional params, nested paths
79
- // -------------------------------------------------------------------------
80
- describe('URL construction', () => {
81
- it('should normalize trailing slashes in base URL', async () => {
82
- const spy = stubFetch(200, { data: [] });
83
- const client = makeClient({ baseUrl: 'https://api.test.com///' });
84
- await client.listProjects();
85
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/projects');
86
- });
87
- it('should omit query string when no optional params are provided', async () => {
88
- const spy = stubFetch(200, { data: [], nextCursor: null });
89
- await makeClient().listProjects();
90
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/projects');
91
- });
92
- it('should include all optional filters as query params', async () => {
93
- const spy = stubFetch(200, { data: [], nextCursor: null });
94
- await makeClient().listProjects({ teamId: 't1', status: 'active', limit: 10 });
95
- const url = spy.mock.calls[0][0];
96
- expect(url).toContain('teamId=t1');
97
- expect(url).toContain('status=active');
98
- expect(url).toContain('limit=10');
99
- });
100
- it('should nest backlog items under the project path', async () => {
101
- const spy = stubFetch(200, { data: [], nextCursor: null });
102
- await makeClient().listBacklogItems('proj-1', { limit: 20 });
103
- const url = spy.mock.calls[0][0];
104
- expect(url).toContain('/v1/projects/proj-1/backlog');
105
- expect(url).toContain('limit=20');
106
- });
107
- it('should nest environments under the project path', async () => {
108
- const spy = stubFetch(200, { data: [] });
109
- await makeClient().listEnvironments('proj-1');
110
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/projects/proj-1/environments');
111
- });
112
- });
113
- // -------------------------------------------------------------------------
114
- // Introspection -- the foundation for capability-gated tool registration
115
- // -------------------------------------------------------------------------
116
- describe('introspection', () => {
117
- it('should return and store the introspection result', async () => {
118
- const expected = {
119
- userId: 'u1',
120
- tenantId: 't1',
121
- email: 'test@example.com',
122
- role: 'member',
123
- capabilities: ['projects:read', 'teams:read'],
124
- };
125
- stubFetch(200, { data: expected });
126
- const client = makeClient();
127
- // Act
128
- const result = await client.introspect();
129
- // Assert -- both return value and stored state
130
- expect(result).toEqual(expected);
131
- expect(client.getIntrospection()).toEqual(expected);
132
- });
133
- it('should report no capabilities before introspect is called', () => {
134
- const client = makeClient();
135
- expect(client.hasCapability('projects:read')).toBe(false);
136
- });
137
- it('should correctly report granted vs missing capabilities', async () => {
138
- const client = await clientWithCapabilities(['projects:read', 'teams:read']);
139
- expect(client.hasCapability('projects:read')).toBe(true);
140
- expect(client.hasCapability('teams:read')).toBe(true);
141
- expect(client.hasCapability('backlog:read')).toBe(false);
142
- expect(client.hasCapability('tenant:documents:write')).toBe(false);
143
- });
144
- it('should report no capabilities when key has empty scope', async () => {
145
- const client = await clientWithCapabilities([]);
146
- expect(client.hasCapability('projects:read')).toBe(false);
147
- });
148
- });
149
- // -------------------------------------------------------------------------
150
- // API methods -- each method hits the right endpoint with the right verb
151
- // These are the HTTP contracts the Taiga API expects.
152
- // -------------------------------------------------------------------------
153
- describe('API contracts', () => {
154
- it('should GET a single project by id', async () => {
155
- const spy = stubFetch(200, { data: { id: 'p1', name: 'Alpha' } });
156
- const result = await makeClient().getProject('p1');
157
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/projects/p1');
158
- expect(result.data).toEqual({ id: 'p1', name: 'Alpha' });
159
- });
160
- it('should GET a single document by type from tenant path', async () => {
161
- const spy = stubFetch(200, { data: { id: 'd1' } });
162
- await makeClient().getTenantDocument('security_policy');
163
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/tenants/current/documents/security_policy');
164
- });
165
- it('should PUT a document draft with the provided body', async () => {
166
- const spy = stubFetch(200, { data: { id: 'd1' } });
167
- await makeClient().saveTenantDocumentDraft('security_policy', { data: { title: 'New' } });
168
- expect(spy.mock.calls[0][1].method).toBe('PUT');
169
- expect(spy.mock.calls[0][0]).toContain('/tenants/current/documents/security_policy/draft');
170
- expect(JSON.parse(spy.mock.calls[0][1].body)).toEqual({
171
- data: { title: 'New' },
172
- });
173
- });
174
- it('should GET a single skill by id from agent-skills path', async () => {
175
- const spy = stubFetch(200, { data: { id: 's1' } });
176
- await makeClient().getSkill('s1');
177
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/agent-skills/s1');
178
- });
179
- it('should include scope filters when listing skills', async () => {
180
- const spy = stubFetch(200, { data: [], nextCursor: null });
181
- await makeClient().listSkills({ scope: 'team', scopeId: 't1', limit: 10 });
182
- const url = spy.mock.calls[0][0];
183
- expect(url).toContain('/v1/agent-skills');
184
- expect(url).toContain('scope=team');
185
- expect(url).toContain('scopeId=t1');
186
- });
187
- it('should GET a single backlog item by id (flat path)', async () => {
188
- const spy = stubFetch(200, { data: { id: 'item-1' } });
189
- await makeClient().getBacklogItem('item-1');
190
- expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/backlog/item-1');
191
- });
192
- it('should GET teams with optional limit', async () => {
193
- const spy = stubFetch(200, { data: [], nextCursor: null });
194
- await makeClient().listTeams({ limit: 50 });
195
- expect(spy.mock.calls[0][0]).toContain('/v1/teams?limit=50');
196
- });
197
- it('should GET documents from tenant path with optional limit', async () => {
198
- const spy = stubFetch(200, { data: [], nextCursor: null });
199
- await makeClient().listTenantDocuments({ limit: 5 });
200
- expect(spy.mock.calls[0][0]).toContain('/v1/tenants/current/documents?limit=5');
201
- });
202
- });
203
- });
package/dist/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=server.integration.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"server.integration.test.d.ts","sourceRoot":"","sources":["../src/server.integration.test.ts"],"names":[],"mappings":""}