@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.
- 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/tools/readiness.ts
DELETED
|
@@ -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
|
-
}
|
package/src/tools/remote.test.ts
DELETED
|
@@ -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
|
-
});
|
package/src/tools/remote.ts
DELETED
|
@@ -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
|
-
}
|