@taiga-ai/mcp-server 0.25.0-rc.474 → 0.26.0-dev.479
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/README.md +111 -0
- package/dist/index.js +292 -115
- package/package.json +15 -2
- package/scripts/build.mjs +0 -37
- package/scripts/prepare-publish.cjs +0 -49
- package/src/client.test.ts +0 -424
- package/src/client.ts +0 -977
- package/src/config.test.ts +0 -24
- package/src/config.ts +0 -28
- package/src/index.ts +0 -45
- package/src/modern.integration.test.ts +0 -153
- package/src/server.integration.test.ts +0 -9214
- package/src/server.ts +0 -18
- package/src/tools/annotations.ts +0 -38
- package/src/tools/autonomy.ts +0 -86
- package/src/tools/backlogs.ts +0 -802
- package/src/tools/builds.ts +0 -668
- package/src/tools/catalogue.test.ts +0 -170
- package/src/tools/catalogue.ts +0 -223
- package/src/tools/context.ts +0 -513
- package/src/tools/documents.ts +0 -375
- package/src/tools/errors.ts +0 -172
- package/src/tools/hints.ts +0 -99
- package/src/tools/inbox.ts +0 -546
- package/src/tools/index.ts +0 -239
- package/src/tools/inputs.ts +0 -47
- package/src/tools/instructions.ts +0 -41
- package/src/tools/jobs.ts +0 -90
- package/src/tools/knowledge.ts +0 -249
- package/src/tools/maintenance.ts +0 -547
- package/src/tools/markdown.test.ts +0 -249
- package/src/tools/markdown.ts +0 -151
- package/src/tools/observe.ts +0 -311
- package/src/tools/product-documents.ts +0 -184
- package/src/tools/products.ts +0 -529
- package/src/tools/prompts.ts +0 -265
- package/src/tools/queue.ts +0 -250
- package/src/tools/readiness.ts +0 -177
- package/src/tools/remote.test.ts +0 -44
- package/src/tools/remote.ts +0 -107
- package/src/tools/repositories.ts +0 -360
- package/src/tools/request-prompt.test.ts +0 -32
- package/src/tools/request-prompt.ts +0 -28
- package/src/tools/resources.ts +0 -217
- package/src/tools/sections.test.ts +0 -143
- package/src/tools/sections.ts +0 -287
- package/src/tools/setup.ts +0 -455
- package/src/tools/summarize.test.ts +0 -20
- package/src/tools/summarize.ts +0 -96
- package/src/tools/wait.ts +0 -490
- package/src/version.test.ts +0 -10
- package/src/version.ts +0 -14
- package/taiga-ai-mcp-server-0.25.0-rc.474.tgz +0 -0
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -9
package/src/config.test.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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.eu.tai.ga' });
|
|
21
|
-
vi.stubEnv('TAIGA_API_URL', 'http://localhost:3001');
|
|
22
|
-
expect(readConfig()?.apiUrl).toBe('http://localhost:3001');
|
|
23
|
-
});
|
|
24
|
-
});
|
package/src/config.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
// The production API behind the custom domain in infra/dns.tf. The old
|
|
9
|
-
// default, api.taiga.app, never resolved, so every install that followed the
|
|
10
|
-
// README without TAIGA_API_URL failed on its first call.
|
|
11
|
-
const DEFAULT_API_URL = 'https://api.eu.tai.ga';
|
|
12
|
-
|
|
13
|
-
const envSchema = z.object({
|
|
14
|
-
TAIGA_API_KEY: z.string().min(1),
|
|
15
|
-
TAIGA_API_URL: z.string().default(DEFAULT_API_URL),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export interface ServerConfig {
|
|
19
|
-
readonly apiKey: string;
|
|
20
|
-
readonly apiUrl: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Null when TAIGA_API_KEY is missing or empty: it is the one setting with no default. */
|
|
24
|
-
export function readConfig(): ServerConfig | null {
|
|
25
|
-
const parsed = envSchema.safeParse(process.env);
|
|
26
|
-
if (!parsed.success) return null;
|
|
27
|
-
return { apiKey: parsed.data.TAIGA_API_KEY, apiUrl: parsed.data.TAIGA_API_URL };
|
|
28
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
4
|
-
import { TaigaClient } from './client.js';
|
|
5
|
-
import { readConfig } from './config.js';
|
|
6
|
-
import { createServer } from './server.js';
|
|
7
|
-
|
|
8
|
-
async function main() {
|
|
9
|
-
const config = readConfig();
|
|
10
|
-
if (!config) {
|
|
11
|
-
console.error('Error: TAIGA_API_KEY environment variable is required.');
|
|
12
|
-
console.error('Create one at https://eu.tai.ga under Settings > Account > API keys.');
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const client = new TaigaClient({ baseUrl: config.apiUrl, apiKey: config.apiKey });
|
|
17
|
-
|
|
18
|
-
// Introspect the API key to discover available capabilities
|
|
19
|
-
const introspection = await client.introspect().catch((error) => {
|
|
20
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
-
console.error(`Error: Failed to validate API key: ${message}`);
|
|
22
|
-
console.error('Check that your TAIGA_API_KEY is valid and not expired.');
|
|
23
|
-
process.exit(1);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const toolCount = introspection.capabilities.length;
|
|
27
|
-
console.error(
|
|
28
|
-
`Taiga MCP server started (${toolCount} capabilities, user: ${introspection.email}, role: ${introspection.role})`
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
// serveStdio owns the protocol revision: a client that opens with
|
|
32
|
-
// server/discover is served 2026-07-28, one that opens with initialize the
|
|
33
|
-
// 2025 revision it asks for, from the same instance. Tools are registered
|
|
34
|
-
// from the key's capabilities, so a key never sees a tool it cannot use.
|
|
35
|
-
// Errors outside a request (the stream failing, a message that cannot be
|
|
36
|
-
// read) go to stderr, which the client shows in its server log.
|
|
37
|
-
serveStdio(() => createServer(client), {
|
|
38
|
-
onerror: (error) => console.error('Taiga MCP server error:', error),
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
main().catch((error) => {
|
|
43
|
-
console.error('Fatal error:', error);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
});
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
|
|
3
|
-
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
4
|
-
import { TaigaClient } from './client.js';
|
|
5
|
-
import { createServer } from './server.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The same server over the 2026-07-28 revision. serveStdio picks the
|
|
9
|
-
* revision from the client's opening exchange, so a client pinned to
|
|
10
|
-
* 2026-07-28 opens with server/discover and every call after it carries the
|
|
11
|
-
* modern envelope. server.integration.test.ts covers the tools in depth over
|
|
12
|
-
* the 2025 handshake; this proves the catalogue, a read, a write, the server
|
|
13
|
-
* instructions, and a wait's progress and cancellation on the new revision.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const ITEM_ID = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5e';
|
|
17
|
-
const PRODUCT_ID = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5c';
|
|
18
|
-
const JOB_ID = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4b10';
|
|
19
|
-
|
|
20
|
-
let close: (() => Promise<void>) | undefined;
|
|
21
|
-
|
|
22
|
-
const json = (body: unknown, status = 200) =>
|
|
23
|
-
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
|
24
|
-
|
|
25
|
-
async function bootModern(capabilities: string[], routes: Record<string, (url: string, init: RequestInit) => unknown>) {
|
|
26
|
-
vi.stubGlobal(
|
|
27
|
-
'fetch',
|
|
28
|
-
vi.fn(async (url: string, init: RequestInit = {}) => {
|
|
29
|
-
const { pathname } = new URL(url);
|
|
30
|
-
if (pathname === '/v1/auth/introspect') {
|
|
31
|
-
return json({ data: { userId: 'u', tenantId: 't', email: 'dev@test.com', role: 'member', capabilities } });
|
|
32
|
-
}
|
|
33
|
-
const route = routes[`${init.method ?? 'GET'} ${pathname}`];
|
|
34
|
-
return route ? route(url, init) : json({ code: 'NOT_FOUND', message: 'No route' }, 404);
|
|
35
|
-
})
|
|
36
|
-
);
|
|
37
|
-
const taiga = new TaigaClient({ baseUrl: 'https://api.test.com', apiKey: 'taiga_sk_live_test' });
|
|
38
|
-
await taiga.introspect();
|
|
39
|
-
|
|
40
|
-
const [clientSide, serverSide] = InMemoryTransport.createLinkedPair();
|
|
41
|
-
const handle = serveStdio(() => createServer(taiga, { wait: { pollIntervalMs: 5 } }), { transport: serverSide });
|
|
42
|
-
const client = new Client(
|
|
43
|
-
{ name: 'modern-test', version: '0.0.1' },
|
|
44
|
-
{ versionNegotiation: { mode: { pin: '2026-07-28' } } }
|
|
45
|
-
);
|
|
46
|
-
await client.connect(clientSide);
|
|
47
|
-
close = async () => {
|
|
48
|
-
await client.close();
|
|
49
|
-
await handle.close();
|
|
50
|
-
};
|
|
51
|
-
return { client };
|
|
52
|
-
}
|
|
53
|
-
afterEach(async () => {
|
|
54
|
-
await close?.();
|
|
55
|
-
close = undefined;
|
|
56
|
-
vi.unstubAllGlobals();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('the server on the 2026-07-28 revision', () => {
|
|
60
|
-
it('negotiates 2026-07-28 and lists the same catalogue, with the server instructions', async () => {
|
|
61
|
-
const { client } = await bootModern(['products:read', 'backlog:read', 'backlog:write', 'products:write'], {});
|
|
62
|
-
|
|
63
|
-
expect(client.getProtocolEra()).toBe('modern');
|
|
64
|
-
expect(client.getInstructions()).toContain('Taiga');
|
|
65
|
-
const names = (await client.listTools()).tools.map((t) => t.name);
|
|
66
|
-
expect(names).toContain('taiga_get_product');
|
|
67
|
-
expect(names).toContain('taiga_request_initiatives');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('reads, writes and refuses an unknown argument as it does on the 2025 revision', async () => {
|
|
71
|
-
let posted: unknown;
|
|
72
|
-
const { client } = await bootModern(['products:read', 'products:write'], {
|
|
73
|
-
[`GET /v1/products/${PRODUCT_ID}`]: () => json({ data: { id: PRODUCT_ID, name: 'Web', slug: 'web' } }),
|
|
74
|
-
'POST /v1/agent-jobs': (_url, init) => {
|
|
75
|
-
posted = JSON.parse(init.body as string);
|
|
76
|
-
return json({ data: { id: JOB_ID, type: 'generate_backlog', status: 'pending' } });
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const product = await client.callTool({ name: 'taiga_get_product', arguments: { productId: PRODUCT_ID } });
|
|
81
|
-
const requested = await client.callTool({
|
|
82
|
-
name: 'taiga_request_initiatives',
|
|
83
|
-
arguments: { productId: PRODUCT_ID, title: 'Health endpoint' },
|
|
84
|
-
});
|
|
85
|
-
const stray = await client.callTool({
|
|
86
|
-
name: 'taiga_get_product',
|
|
87
|
-
arguments: { productId: PRODUCT_ID, note: 'not an argument' },
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
expect(JSON.parse((product.content as { text: string }[])[0]!.text)).toMatchObject({ id: PRODUCT_ID, name: 'Web' });
|
|
91
|
-
expect(posted).toMatchObject({ scope: 'product', type: 'generate_backlog', targetId: PRODUCT_ID });
|
|
92
|
-
expect(JSON.parse((requested.content as { text: string }[])[0]!.text)).toMatchObject({ id: JOB_ID });
|
|
93
|
-
expect(stray.isError).toBe(true);
|
|
94
|
-
expect((stray.content as { text: string }[])[0]!.text).toMatch(/Unrecognized key/);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('reports progress while a wait runs, and ends the read in flight when the call is cancelled', async () => {
|
|
98
|
-
let reads = 0;
|
|
99
|
-
let hung: AbortSignal | undefined;
|
|
100
|
-
const job = (status: string) => json({ data: { id: JOB_ID, type: 'refresh_reality', status } });
|
|
101
|
-
const { client } = await bootModern(['products:read', 'backlog:read'], {
|
|
102
|
-
[`GET /v1/agent-jobs/${JOB_ID}`]: () => job(++reads < 3 ? 'processing' : 'completed'),
|
|
103
|
-
[`GET /v1/backlog/${ITEM_ID}`]: (_url, init) => {
|
|
104
|
-
hung = init.signal!;
|
|
105
|
-
return new Promise((_resolve, reject) =>
|
|
106
|
-
init.signal!.addEventListener('abort', () => reject(init.signal!.reason))
|
|
107
|
-
);
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
const onprogress = vi.fn();
|
|
111
|
-
|
|
112
|
-
const waited = await client.callTool({ name: 'taiga_wait_for_job', arguments: { jobId: JOB_ID } }, { onprogress });
|
|
113
|
-
expect(JSON.parse((waited.content as { text: string }[])[0]!.text)).toMatchObject({ reached: true });
|
|
114
|
-
expect(onprogress).toHaveBeenCalledWith(expect.objectContaining({ message: 'Still waiting' }));
|
|
115
|
-
|
|
116
|
-
const controller = new AbortController();
|
|
117
|
-
const pending = client.callTool(
|
|
118
|
-
{ name: 'taiga_wait_for_initiative', arguments: { initiativeId: ITEM_ID, until: 'planned', timeoutSeconds: 60 } },
|
|
119
|
-
{ signal: controller.signal }
|
|
120
|
-
);
|
|
121
|
-
await vi.waitFor(() => expect(hung).toBeDefined());
|
|
122
|
-
controller.abort();
|
|
123
|
-
await expect(pending).rejects.toThrow();
|
|
124
|
-
await vi.waitFor(() => expect(hung!.aborted).toBe(true));
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('reads a resource, gets a prompt and refuses a missing document with invalid params', async () => {
|
|
128
|
-
const policy = {
|
|
129
|
-
id: 'doc-1',
|
|
130
|
-
type: 'policy_security',
|
|
131
|
-
name: 'Security Policy',
|
|
132
|
-
version: 3,
|
|
133
|
-
isDraft: false,
|
|
134
|
-
data: { version: '1.0.0', sections: { summary: { source: 'template', priority: 'core', content: 'Encrypt' } } },
|
|
135
|
-
};
|
|
136
|
-
const { client } = await bootModern(['tenant:documents:read'], {
|
|
137
|
-
'GET /v1/tenants/current/documents/policy_security': () => json({ data: policy }),
|
|
138
|
-
'GET /v1/tenants/current/documents': () => json({ data: [policy], nextCursor: null }),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const { contents } = await client.readResource({ uri: 'taiga://documents/policy_security' });
|
|
142
|
-
const prompt = await client.getPrompt({
|
|
143
|
-
name: 'taiga_review_against_policies',
|
|
144
|
-
arguments: { policyType: 'policy_security' },
|
|
145
|
-
});
|
|
146
|
-
const missing = await client.readResource({ uri: 'taiga://documents/tone_of_voice' }).catch((e: unknown) => e);
|
|
147
|
-
|
|
148
|
-
expect((contents[0] as { text: string }).text).toContain('# Security Policy');
|
|
149
|
-
expect((prompt.messages[0]!.content as { text: string }).text).toContain('# Security Policy');
|
|
150
|
-
expect(missing).toMatchObject({ code: -32602 });
|
|
151
|
-
expect(String((missing as Error).message)).toMatch(/^No published tone_of_voice/);
|
|
152
|
-
});
|
|
153
|
-
});
|