@taiga-ai/mcp-server 0.25.0-rc.474 → 0.26.0-dev.481

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 (55) hide show
  1. package/README.md +111 -0
  2. package/dist/index.js +292 -115
  3. package/package.json +15 -2
  4. package/scripts/build.mjs +0 -37
  5. package/scripts/prepare-publish.cjs +0 -49
  6. package/src/client.test.ts +0 -424
  7. package/src/client.ts +0 -977
  8. package/src/config.test.ts +0 -24
  9. package/src/config.ts +0 -28
  10. package/src/index.ts +0 -45
  11. package/src/modern.integration.test.ts +0 -153
  12. package/src/server.integration.test.ts +0 -9214
  13. package/src/server.ts +0 -18
  14. package/src/tools/annotations.ts +0 -38
  15. package/src/tools/autonomy.ts +0 -86
  16. package/src/tools/backlogs.ts +0 -802
  17. package/src/tools/builds.ts +0 -668
  18. package/src/tools/catalogue.test.ts +0 -170
  19. package/src/tools/catalogue.ts +0 -223
  20. package/src/tools/context.ts +0 -513
  21. package/src/tools/documents.ts +0 -375
  22. package/src/tools/errors.ts +0 -172
  23. package/src/tools/hints.ts +0 -99
  24. package/src/tools/inbox.ts +0 -546
  25. package/src/tools/index.ts +0 -239
  26. package/src/tools/inputs.ts +0 -47
  27. package/src/tools/instructions.ts +0 -41
  28. package/src/tools/jobs.ts +0 -90
  29. package/src/tools/knowledge.ts +0 -249
  30. package/src/tools/maintenance.ts +0 -547
  31. package/src/tools/markdown.test.ts +0 -249
  32. package/src/tools/markdown.ts +0 -151
  33. package/src/tools/observe.ts +0 -311
  34. package/src/tools/product-documents.ts +0 -184
  35. package/src/tools/products.ts +0 -529
  36. package/src/tools/prompts.ts +0 -265
  37. package/src/tools/queue.ts +0 -250
  38. package/src/tools/readiness.ts +0 -177
  39. package/src/tools/remote.test.ts +0 -44
  40. package/src/tools/remote.ts +0 -107
  41. package/src/tools/repositories.ts +0 -360
  42. package/src/tools/request-prompt.test.ts +0 -32
  43. package/src/tools/request-prompt.ts +0 -28
  44. package/src/tools/resources.ts +0 -217
  45. package/src/tools/sections.test.ts +0 -143
  46. package/src/tools/sections.ts +0 -287
  47. package/src/tools/setup.ts +0 -455
  48. package/src/tools/summarize.test.ts +0 -20
  49. package/src/tools/summarize.ts +0 -96
  50. package/src/tools/wait.ts +0 -490
  51. package/src/version.test.ts +0 -10
  52. package/src/version.ts +0 -14
  53. package/taiga-ai-mcp-server-0.25.0-rc.474.tgz +0 -0
  54. package/tsconfig.json +0 -21
  55. package/vitest.config.ts +0 -9
@@ -1,177 +0,0 @@
1
- import { TaigaApiError, type TaigaClient } from '../client.js';
2
- import { PRODUCT_DOCUMENT_NAMES, isImportedProduct, setupNextStep, type ProductSettingsView } from './catalogue.js';
3
- import { isNetworkFailure } from './errors.js';
4
-
5
- type Row = Record<string, unknown>;
6
-
7
- /**
8
- * What a product in setup still needs, read rather than learned from a
9
- * refused write (PRO-2800). A reviewing agent linked a repository and picked
10
- * a branch, and every read still said "link its repository": nextStep read
11
- * the Discovery step alone, and the only way to see the missing documents was
12
- * to call taiga_finish_discovery and read its refusal. This reads the same
13
- * preconditions the API refuses on (GET /products/:id/setup), the linked
14
- * repositories and the import, and says the one next move.
15
- */
16
- export interface Readiness {
17
- /** Whether taiga_finish_discovery would succeed now. Null when the API could not say. */
18
- ready: boolean | null;
19
- /** Null when the links could not be read: not the same as none linked. */
20
- repository: { linked: false } | { linked: true; name: unknown; branch: unknown } | null;
21
- /** An imported product's import, as its settings.importStatus says it: pending (not started), analyzing or generating_docs (running), complete, failed, cancelled. */
22
- import?: string;
23
- discoveryStep?: unknown;
24
- /** The required documents not yet published, as the type taiga_get_product_document takes; null when the API could not say. */
25
- missingDocuments: string[] | null;
26
- /** The required checks not yet stamped (stamped in the web); null when the API could not say. */
27
- openChecks: string[] | null;
28
- /** The one move that gets it closer; null when too little could be read to say. */
29
- nextStep: string | null;
30
- }
31
-
32
- const IMPORT_STATE: Record<string, string> = {
33
- pending: 'not started',
34
- analyzing: 'running',
35
- generating_docs: 'running',
36
- complete: 'complete',
37
- failed: 'failed',
38
- cancelled: 'failed',
39
- };
40
-
41
- /** A failure that is the API's (refused, missing, unreachable), not a bug in this code. */
42
- function isApiSide(error: unknown): boolean {
43
- return error instanceof TaigaApiError || isNetworkFailure(error);
44
- }
45
-
46
- /** The documents by the names the web uses. */
47
- function named(types: string[]): string {
48
- return types.map((type) => PRODUCT_DOCUMENT_NAMES[type as keyof typeof PRODUCT_DOCUMENT_NAMES] ?? type).join(', ');
49
- }
50
-
51
- /** The product's setup readiness, or null once it has finished setup. */
52
- export async function setupReadiness(
53
- client: TaigaClient,
54
- product: Row,
55
- /** The product's repository links when the caller is reading them anyway (the context), so one read serves both. */
56
- links?: () => Promise<{ data: unknown[] }>
57
- ): Promise<Readiness | null> {
58
- if (product['status'] !== 'draft') return null;
59
- const id = String(product['id']);
60
- const imported = isImportedProduct(product as ProductSettingsView);
61
- const [repos, blockers, full] = await Promise.all([
62
- (links ? links() : client.listProductRepositories(id)).then(
63
- (result) => result.data as Row[],
64
- (error: unknown) => {
65
- if (isApiSide(error)) return null;
66
- throw error;
67
- }
68
- ),
69
- // An API from before the read answers 404 to it: fall back to the step.
70
- client.getProductSetupBlockers(id).then(
71
- (result) => result.data,
72
- (error: unknown) => {
73
- if (isApiSide(error)) return null;
74
- throw error;
75
- }
76
- ),
77
- // A write's answer carries no discoveryStep; the read derives it.
78
- 'discoveryStep' in product || imported
79
- ? Promise.resolve(product)
80
- : client.getProduct(id).then(
81
- (result) => result.data as Row,
82
- (error: unknown) => {
83
- if (isApiSide(error)) return product;
84
- throw error;
85
- }
86
- ),
87
- ]);
88
-
89
- const link = repos?.[0];
90
- const repository = !repos
91
- ? null
92
- : link
93
- ? { linked: true as const, name: link['repositoryName'], branch: link['effectiveBranch'] }
94
- : { linked: false as const };
95
- const settings = (product['settings'] ?? {}) as { importStatus?: unknown };
96
- const importState = imported ? (IMPORT_STATE[String(settings.importStatus)] ?? 'not started') : undefined;
97
- const missing = blockers ? blockers.missingDocuments : null;
98
- const openChecks = blockers ? blockers.openChecks.map((check) => check.criterion) : null;
99
- // What the API refuses on, nothing else: a running import is in nextStep, not a blocker.
100
- const ready = blockers ? missing!.length === 0 && openChecks!.length === 0 : null;
101
-
102
- return {
103
- ready,
104
- repository,
105
- ...(imported ? { import: typeof settings.importStatus === 'string' ? settings.importStatus : 'pending' } : {}),
106
- ...(imported ? {} : { discoveryStep: full['discoveryStep'] ?? null }),
107
- missingDocuments: missing,
108
- openChecks,
109
- nextStep: nextStepFor({
110
- imported,
111
- importState,
112
- linked: repository?.linked ?? null,
113
- missing,
114
- openChecks,
115
- product: full,
116
- }),
117
- };
118
- }
119
-
120
- function nextStepFor(state: {
121
- imported: boolean;
122
- importState: string | undefined;
123
- /** Null when the links could not be read. */
124
- linked: boolean | null;
125
- missing: string[] | null;
126
- openChecks: string[] | null;
127
- product: Row;
128
- }): string | null {
129
- const finish =
130
- 'taiga_finish_discovery opens the product and locks its documents (ask the person: it is their decision ' +
131
- 'that the documents are right).';
132
- if (state.imported) {
133
- if (state.linked === false) {
134
- return 'Link its repository (taiga_list_linkable_repositories, then taiga_link_repository), then taiga_start_import.';
135
- }
136
- if (state.linked === null && state.importState === 'not started')
137
- return setupNextStep(state.product as ProductSettingsView);
138
- switch (state.importState) {
139
- case 'running':
140
- return 'The import is running and derives the documents from the code; taiga_wait_for_job follows it.';
141
- case 'failed':
142
- return 'The last import did not finish: taiga_start_import runs it again.';
143
- case 'complete':
144
- if (state.missing && state.missing.length > 0) {
145
- return (
146
- `The import finished but ${named(state.missing)} ${state.missing.length === 1 ? 'is' : 'are'} not ` +
147
- 'published: taiga_get_product_document without type shows them, taiga_publish_product_document publishes a draft.'
148
- );
149
- }
150
- return `The import is done: review the reality documents (taiga_get_product_document without type), then ${finish}`;
151
- default:
152
- return 'taiga_start_import derives the documents from the code and runs by itself (paid agent work: ask first).';
153
- }
154
- }
155
- // Without the API's read, the Discovery step is all there is to go on, and
156
- // without that, nothing: a guess would send a product at its documents back
157
- // to the conversation.
158
- if (!state.missing) return setupNextStep(state.product as ProductSettingsView);
159
- if (state.missing.includes('specification')) {
160
- return (
161
- 'The specification comes from the Discovery conversation, held in the web: the person describes the ' +
162
- 'product there. The other documents follow from it.' +
163
- (state.linked === false
164
- ? ' Linking a repository (taiga_link_repository) can wait until before the first build.'
165
- : '')
166
- );
167
- }
168
- if (state.missing.length > 0) {
169
- return (
170
- `Still to publish: ${named(state.missing)}. taiga_generate_product_documents generates the missing ones ` +
171
- '(paid agent work: ask first); a draft is published with taiga_publish_product_document.'
172
- );
173
- }
174
- if (state.openChecks && state.openChecks.length > 0)
175
- return `The documents are published; the checks are stamped in the web, then ${finish}`;
176
- return `Every required document is published: ${finish}`;
177
- }
@@ -1,44 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { parseRemote } from './remote.js';
3
-
4
- describe('parseRemote', () => {
5
- it.each([
6
- ['https://github.com/acme/web.git', 'https://github.com', 'acme/web'],
7
- ['https://github.com/acme/web', 'https://github.com', 'acme/web'],
8
- ['https://github.com/acme/web/', 'https://github.com', 'acme/web'],
9
- ['https://github.com/acme/web.git/', 'https://github.com', 'acme/web'],
10
- ['git@github.com:acme/web.git/', 'https://github.com', 'acme/web'],
11
- ['https://user@GitHub.com/Acme/Web.git', 'https://github.com', 'Acme/Web'],
12
- ['http://ghe.local:8443/acme/web.git', 'http://ghe.local:8443', 'acme/web'],
13
- ['ssh://git@github.com/acme/web.git', 'https://github.com', 'acme/web'],
14
- ['ssh://git@ghe.example.com:2222/acme/web.git', 'https://ghe.example.com', 'acme/web'],
15
- ['git+ssh://git@github.com/acme/web.git', 'https://github.com', 'acme/web'],
16
- ['git@github.com:acme/web.git', 'https://github.com', 'acme/web'],
17
- ['git@taiga.ghe.com:taiga/taiga-app.git', 'https://taiga.ghe.com', 'taiga/taiga-app'],
18
- [' git@github.com:acme/web.git\n', 'https://github.com', 'acme/web'],
19
- ['https://g/acme/web', 'https://g', 'acme/web'],
20
- ])('reads %s as %s %s', (remote, host, repository) => {
21
- expect(parseRemote(remote)).toEqual({ host, repository });
22
- });
23
-
24
- it.each([
25
- [''],
26
- ['not a remote'],
27
- ['https://github.com/acme'],
28
- ['https://gitlab.com/group/subgroup/repo.git'],
29
- ['https://github.com/acme/../web'],
30
- ['https://github.com/acme/tmp/../web'],
31
- ['https://github.com/acme/tmp/%2e%2e/web'],
32
- ['https://github.com/acme/./web'],
33
- ['git@github.com:acme/tmp/../web.git'],
34
- ['ftp://github.com/acme/web'],
35
- ['/Users/dev/acme/web'],
36
- ['C:/acme/web'],
37
- ['git@ghe.local:8443:acme/web.git'],
38
- ['git@github[.]com:acme/web.git'],
39
- ['git@git hub.com:acme/web.git'],
40
- ['acme/web'],
41
- ])('refuses %s', (remote) => {
42
- expect(parseRemote(remote)).toBeNull();
43
- });
44
- });
@@ -1,107 +0,0 @@
1
- /**
2
- * What a git remote says about where a repository lives, in the terms Taiga
3
- * stores: the web origin of the host and the `owner/repo` name. A developer's
4
- * checkout knows its remote, not its Taiga product; this is the bridge.
5
- *
6
- * Accepted shapes: `https://host/owner/repo(.git)`, `ssh://git@host/owner/repo`,
7
- * `git+ssh://...` and the scp-like `git@host:owner/repo.git`. Anything else,
8
- * and any path that is not exactly two segments, is null: Taiga links
9
- * `owner/repo` names only, so a deeper path can never match a link.
10
- *
11
- * An SSH remote names no web origin, so it is read as `https://host` on the
12
- * default port. A host whose web UI sits on another port or on plain http
13
- * does not resolve from its SSH remote; the https remote does.
14
- */
15
- export interface ParsedRemote {
16
- /** The web origin the link's URL starts with, such as `https://github.com`. */
17
- host: string;
18
- /** `owner/repo`, in the caller's case; the API matches it case-insensitively. */
19
- repository: string;
20
- }
21
-
22
- const SCP_LIKE = /^(?:[^@\s/]+@)?([^:\s/]+):([^\s]+)$/;
23
-
24
- export function parseRemote(remote: string): ParsedRemote | null {
25
- const trimmed = remote.trim();
26
- if (trimmed === '') return null;
27
-
28
- let host: string;
29
- let path: string;
30
- let scheme = 'https';
31
-
32
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
33
- // The URL parser folds `..` away before the path can be looked at, so
34
- // the raw text after the authority is checked first: a dot segment in a
35
- // remote is never what `git remote get-url` prints.
36
- if (hasDotSegment(trimmed.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/i, ''))) return null;
37
- let url: URL;
38
- try {
39
- url = new URL(trimmed);
40
- } catch {
41
- return null;
42
- }
43
- const protocol = url.protocol.replace(/:$/, '').toLowerCase();
44
- if (protocol === 'http' || protocol === 'https') {
45
- scheme = protocol;
46
- // The port is part of the web origin a link was stored with.
47
- host = url.port ? `${url.hostname}:${url.port}` : url.hostname;
48
- } else if (protocol === 'ssh' || protocol === 'git+ssh' || protocol === 'git') {
49
- // An SSH port says nothing about the web origin.
50
- host = url.hostname;
51
- } else {
52
- return null;
53
- }
54
- path = url.pathname;
55
- } else {
56
- const match = SCP_LIKE.exec(trimmed);
57
- if (!match) return null;
58
- host = match[1]!;
59
- path = match[2]!;
60
- // Only this shape can misread a Windows drive (`C:/acme/web`) as a host;
61
- // an explicit URL with a one-letter hostname is what it says it is.
62
- if (host.length < 2) return null;
63
- }
64
-
65
- host = host.toLowerCase();
66
- if (host === '') return null;
67
-
68
- const segments = path
69
- .replace(/\/+$/, '')
70
- .replace(/\.git$/i, '')
71
- .split('/')
72
- .filter((segment) => segment !== '');
73
- if (segments.length !== 2) return null;
74
- // `.`/`..` would collapse into another route; a ':' is a port that landed
75
- // in the path (`git@host:8443:owner/repo`), which no link name carries.
76
- if (hasDotSegment(path) || segments.some((segment) => segment.includes(':'))) return null;
77
-
78
- // The scp-like shape takes any host-looking text; only an origin the URL
79
- // parser accepts is sent on, so `git@github[.]com:acme/web` is refused
80
- // here rather than by the API after a request.
81
- const origin = webOrigin(`${scheme}://${host}`);
82
- if (origin === null) return null;
83
-
84
- return { host: origin, repository: `${segments[0]}/${segments[1]}` };
85
- }
86
-
87
- /** Whether any segment of the path is `.` or `..`, percent-encoded or not. */
88
- function hasDotSegment(path: string): boolean {
89
- return path.split('/').some((segment) => {
90
- let decoded = segment;
91
- try {
92
- decoded = decodeURIComponent(segment);
93
- } catch {
94
- // An undecodable segment is kept as is; it is not a dot segment.
95
- }
96
- return decoded === '.' || decoded === '..';
97
- });
98
- }
99
-
100
- function webOrigin(url: string): string | null {
101
- try {
102
- const origin = new URL(url).origin;
103
- return origin === 'null' ? null : origin;
104
- } catch {
105
- return null;
106
- }
107
- }