@taiga-ai/mcp-server 0.14.0-rc.426 → 0.16.0-dev.430
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 +560 -158
- package/package.json +6 -2
- package/scripts/build.mjs +2 -2
- package/src/client.ts +130 -2
- package/src/index.ts +11 -21
- package/src/modern.integration.test.ts +153 -0
- package/src/server.integration.test.ts +612 -44
- package/src/server.ts +18 -0
- package/src/tools/autonomy.ts +5 -5
- package/src/tools/backlogs.ts +20 -20
- package/src/tools/builds.ts +6 -6
- package/src/tools/catalogue.test.ts +5 -3
- package/src/tools/context.ts +3 -4
- package/src/tools/documents.ts +16 -16
- package/src/tools/errors.ts +7 -19
- package/src/tools/inbox.ts +6 -6
- package/src/tools/index.ts +11 -8
- package/src/tools/instructions.ts +8 -0
- package/src/tools/jobs.ts +2 -2
- package/src/tools/knowledge.ts +7 -7
- package/src/tools/maintenance.ts +13 -13
- package/src/tools/observe.ts +12 -8
- package/src/tools/product-documents.ts +8 -8
- package/src/tools/products.ts +12 -12
- package/src/tools/prompts.ts +11 -17
- package/src/tools/queue.ts +4 -4
- package/src/tools/repositories.ts +266 -0
- package/src/tools/resources.ts +9 -5
- package/src/tools/setup.ts +342 -0
- package/src/tools/wait.ts +21 -21
- package/taiga-ai-mcp-server-0.16.0-dev.430.tgz +0 -0
- package/taiga-ai-mcp-server-0.14.0-rc.426.tgz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taiga-ai/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0-dev.430",
|
|
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",
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"default": "./dist/index.js"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
16
19
|
"scripts": {
|
|
17
20
|
"build": "tsc --noEmit && node scripts/build.mjs",
|
|
18
21
|
"typecheck": "tsc --noEmit",
|
|
@@ -22,10 +25,11 @@
|
|
|
22
25
|
"test": "vitest run"
|
|
23
26
|
},
|
|
24
27
|
"dependencies": {
|
|
25
|
-
"@modelcontextprotocol/
|
|
28
|
+
"@modelcontextprotocol/server": "^2.3.1",
|
|
26
29
|
"zod": "^4.4.3"
|
|
27
30
|
},
|
|
28
31
|
"devDependencies": {
|
|
32
|
+
"@modelcontextprotocol/client": "^2.3.1",
|
|
29
33
|
"@tsconfig/strictest": "^2.0.8",
|
|
30
34
|
"@types/mdast": "^4.0.4",
|
|
31
35
|
"@types/node": "^22.20.1",
|
package/scripts/build.mjs
CHANGED
|
@@ -9,10 +9,10 @@ const SHEBANG = '#!/usr/bin/env node';
|
|
|
9
9
|
*
|
|
10
10
|
* The published npm artifact must not reference private workspace packages, so
|
|
11
11
|
* `@taiga/*` (notably `@taiga/feature-flags`, a runtime value) is inlined. The
|
|
12
|
-
* genuinely-published deps — the MCP SDK and zod — stay external and are
|
|
12
|
+
* genuinely-published deps — the MCP server SDK and zod — stay external and are
|
|
13
13
|
* installed normally by consumers. Node built-ins are external via platform.
|
|
14
14
|
*/
|
|
15
|
-
const external = [/^@modelcontextprotocol\/
|
|
15
|
+
const external = [/^@modelcontextprotocol\/server(\/|$)/, /^zod(\/|$)/, /^node:/];
|
|
16
16
|
|
|
17
17
|
const bundle = await rolldown({
|
|
18
18
|
input: 'src/index.ts',
|
package/src/client.ts
CHANGED
|
@@ -45,6 +45,8 @@ export class TaigaApiError extends Error {
|
|
|
45
45
|
readonly status: number;
|
|
46
46
|
readonly code: string | undefined;
|
|
47
47
|
readonly apiMessage: string | undefined;
|
|
48
|
+
/** A refusal's structured reason, when the API sent one: `{ reason, ... }` (a 409's discriminator and its data). */
|
|
49
|
+
readonly details: Record<string, unknown> | undefined;
|
|
48
50
|
|
|
49
51
|
constructor(method: string, path: string, status: number, body: string) {
|
|
50
52
|
const parsed = parseErrorBody(body);
|
|
@@ -53,6 +55,7 @@ export class TaigaApiError extends Error {
|
|
|
53
55
|
this.status = status;
|
|
54
56
|
this.code = parsed.code;
|
|
55
57
|
this.apiMessage = parsed.message;
|
|
58
|
+
this.details = parsed.details;
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -84,7 +87,7 @@ function query(params: Record<string, string | number | boolean | null | undefin
|
|
|
84
87
|
return text ? `?${text}` : '';
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
function parseErrorBody(body: string): { code?: string; message?: string } {
|
|
90
|
+
function parseErrorBody(body: string): { code?: string; message?: string; details?: Record<string, unknown> } {
|
|
88
91
|
try {
|
|
89
92
|
const json = JSON.parse(body) as { code?: unknown; message?: unknown; details?: unknown };
|
|
90
93
|
const message = typeof json.message === 'string' ? json.message : undefined;
|
|
@@ -97,9 +100,16 @@ function parseErrorBody(body: string): { code?: string; message?: string } {
|
|
|
97
100
|
)
|
|
98
101
|
.filter(Boolean)
|
|
99
102
|
: [];
|
|
103
|
+
// A service refusal names its gate in an object (`reason`, and what it
|
|
104
|
+
// found, such as the documents still missing); the model needs both.
|
|
105
|
+
const structured =
|
|
106
|
+
typeof json.details === 'object' && json.details !== null && !Array.isArray(json.details)
|
|
107
|
+
? (json.details as Record<string, unknown>)
|
|
108
|
+
: undefined;
|
|
100
109
|
return {
|
|
101
110
|
...(typeof json.code === 'string' ? { code: json.code } : {}),
|
|
102
111
|
...(message ? { message: details.length ? `${message}: ${details.join('; ')}` : message } : {}),
|
|
112
|
+
...(structured ? { details: structured } : {}),
|
|
103
113
|
};
|
|
104
114
|
} catch {
|
|
105
115
|
return {};
|
|
@@ -753,7 +763,15 @@ export class TaigaClient {
|
|
|
753
763
|
|
|
754
764
|
async updateProduct(
|
|
755
765
|
productId: string,
|
|
756
|
-
body: {
|
|
766
|
+
body: {
|
|
767
|
+
autoMerge?: boolean | null | undefined;
|
|
768
|
+
autoBuild?: boolean | undefined;
|
|
769
|
+
name?: string | undefined;
|
|
770
|
+
ownsInfrastructure?: boolean | null | undefined;
|
|
771
|
+
ownsDeployment?: boolean | null | undefined;
|
|
772
|
+
deploymentNotes?: string | null | undefined;
|
|
773
|
+
emitDesignStandards?: boolean | null | undefined;
|
|
774
|
+
}
|
|
757
775
|
) {
|
|
758
776
|
return this.request<{ data: unknown }>('PATCH', `/products/${seg(productId)}`, body);
|
|
759
777
|
}
|
|
@@ -777,4 +795,114 @@ export class TaigaClient {
|
|
|
777
795
|
const qs = params.toString();
|
|
778
796
|
return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `/factories${qs ? `?${qs}` : ''}`);
|
|
779
797
|
}
|
|
798
|
+
|
|
799
|
+
async createFactory(body: { name: string }) {
|
|
800
|
+
return this.request<{ data: unknown }>('POST', '/factories', body);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async updateFactory(factoryId: string, body: { name?: string | undefined }) {
|
|
804
|
+
return this.request<{ data: unknown }>('PATCH', `/factories/${seg(factoryId)}`, body);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// ---------------------------------------------------------------------------
|
|
808
|
+
// Setting a product up
|
|
809
|
+
// ---------------------------------------------------------------------------
|
|
810
|
+
|
|
811
|
+
async createProduct(body: {
|
|
812
|
+
name: string;
|
|
813
|
+
factoryId: string;
|
|
814
|
+
projectType?: 'brownfield' | undefined;
|
|
815
|
+
ownsInfrastructure?: boolean | null | undefined;
|
|
816
|
+
ownsDeployment?: boolean | null | undefined;
|
|
817
|
+
deploymentNotes?: string | undefined;
|
|
818
|
+
}) {
|
|
819
|
+
return this.request<{ data: unknown }>('POST', '/products', body);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async previewProductMove(productId: string, factoryId: string) {
|
|
823
|
+
return this.request<{ data: unknown }>('GET', `/products/${seg(productId)}/factory/preview${query({ factoryId })}`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async moveProduct(productId: string, body: { factoryId: string; stopLine?: boolean | undefined }) {
|
|
827
|
+
return this.request<Record<string, unknown>>('PUT', `/products/${seg(productId)}/factory`, body);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async startImport(productId: string) {
|
|
831
|
+
return this.request<{ data: { jobId: string } }>('POST', `/products/${seg(productId)}/brownfield-import`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async refreshRealityDocuments(productId: string, docTypes?: readonly string[] | undefined) {
|
|
835
|
+
return this.request<{ data: { jobId: string } }>(
|
|
836
|
+
'POST',
|
|
837
|
+
`/products/${seg(productId)}/documents/refresh-reality`,
|
|
838
|
+
docTypes ? { docTypes } : undefined
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async completeSetup(productId: string) {
|
|
843
|
+
return this.request<{ data: unknown }>('POST', `/products/${seg(productId)}/setup/complete`);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async reopenSetup(productId: string) {
|
|
847
|
+
return this.request<{ data: unknown }>('POST', `/products/${seg(productId)}/setup/reopen`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ---------------------------------------------------------------------------
|
|
851
|
+
// Repositories
|
|
852
|
+
// ---------------------------------------------------------------------------
|
|
853
|
+
|
|
854
|
+
/** Every connected GitHub account, page by page: a later account must be findable by its id too. */
|
|
855
|
+
async listGithubInstallations() {
|
|
856
|
+
const data: unknown[] = [];
|
|
857
|
+
let cursor: string | undefined;
|
|
858
|
+
do {
|
|
859
|
+
const result = await this.request<{ data: unknown[]; nextCursor: string | null }>(
|
|
860
|
+
'GET',
|
|
861
|
+
`/github/installations${query({ limit: 100, cursor })}`
|
|
862
|
+
);
|
|
863
|
+
data.push(...result.data);
|
|
864
|
+
cursor = result.nextCursor ?? undefined;
|
|
865
|
+
} while (cursor);
|
|
866
|
+
return { data };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async listInstallationRepositories(
|
|
870
|
+
installationId: string,
|
|
871
|
+
opts: { q?: string | undefined; limit?: number | undefined; cursor?: string | undefined }
|
|
872
|
+
) {
|
|
873
|
+
return this.request<{ data: unknown[]; nextCursor: string | null }>(
|
|
874
|
+
'GET',
|
|
875
|
+
`/github/installations/${seg(installationId)}/repositories${query({ ...opts })}`
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async linkRepository(
|
|
880
|
+
productId: string,
|
|
881
|
+
body: { githubRepositoryId: string; replacesRepositoryId?: string | undefined }
|
|
882
|
+
) {
|
|
883
|
+
return this.request<{ data: unknown }>('POST', `/products/${seg(productId)}/repositories`, body);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async updateRepository(
|
|
887
|
+
productId: string,
|
|
888
|
+
repositoryId: string,
|
|
889
|
+
body: { baseBranch?: string | null | undefined; label?: string | null | undefined }
|
|
890
|
+
) {
|
|
891
|
+
return this.request<{ data: unknown }>(
|
|
892
|
+
'PATCH',
|
|
893
|
+
`/products/${seg(productId)}/repositories/${seg(repositoryId)}`,
|
|
894
|
+
body
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async listRepositoryBranches(productId: string, repositoryId: string) {
|
|
899
|
+
return this.request<{ data: unknown[]; truncated: boolean }>(
|
|
900
|
+
'GET',
|
|
901
|
+
`/products/${seg(productId)}/repositories/${seg(repositoryId)}/branches`
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async unlinkRepository(productId: string, repositoryId: string) {
|
|
906
|
+
return this.request<null>('DELETE', `/products/${seg(productId)}/repositories/${seg(repositoryId)}`);
|
|
907
|
+
}
|
|
780
908
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
5
4
|
import { TaigaClient } from './client.js';
|
|
6
5
|
import { readConfig } from './config.js';
|
|
7
|
-
import {
|
|
8
|
-
import { SERVER_INSTRUCTIONS } from './tools/instructions.js';
|
|
9
|
-
import { SERVER_VERSION } from './version.js';
|
|
6
|
+
import { createServer } from './server.js';
|
|
10
7
|
|
|
11
8
|
async function main() {
|
|
12
9
|
const config = readConfig();
|
|
@@ -26,27 +23,20 @@ async function main() {
|
|
|
26
23
|
process.exit(1);
|
|
27
24
|
});
|
|
28
25
|
|
|
29
|
-
// Create MCP server with dynamic tool registration and instructions
|
|
30
|
-
const server = new McpServer(
|
|
31
|
-
{
|
|
32
|
-
name: 'taiga',
|
|
33
|
-
version: SERVER_VERSION,
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
instructions: SERVER_INSTRUCTIONS,
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
registerAllTools(server, client);
|
|
41
|
-
|
|
42
26
|
const toolCount = introspection.capabilities.length;
|
|
43
27
|
console.error(
|
|
44
28
|
`Taiga MCP server started (${toolCount} capabilities, user: ${introspection.email}, role: ${introspection.role})`
|
|
45
29
|
);
|
|
46
30
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
});
|
|
50
40
|
}
|
|
51
41
|
|
|
52
42
|
main().catch((error) => {
|
|
@@ -0,0 +1,153 @@
|
|
|
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_backlog_item', arguments: { itemId: 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
|
+
});
|