@schoolai/shipyard 1.2.0 → 2.0.0
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/{auth-XINVA3ZW.js → auth-V6KVU7VA.js} +4 -3
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/{chunk-FY3DRRGT.js → chunk-5OHYOIOG.js} +3 -3
- package/dist/chunk-5OHYOIOG.js.map +1 -0
- package/dist/{chunk-HS57GMAL.js → chunk-6AQKMCGK.js} +15 -9
- package/dist/chunk-6AQKMCGK.js.map +1 -0
- package/dist/chunk-BYFLMOF7.js +591 -0
- package/dist/chunk-BYFLMOF7.js.map +1 -0
- package/dist/{login-Q6PDH6HF.js → chunk-C4SKAWEG.js} +104 -40
- package/dist/chunk-C4SKAWEG.js.map +1 -0
- package/dist/chunk-DPMRSLYJ.js +109 -0
- package/dist/chunk-DPMRSLYJ.js.map +1 -0
- package/dist/{chunk-K3GFMEBF.js → chunk-JFEQEK53.js} +3 -3
- package/dist/chunk-JFEQEK53.js.map +1 -0
- package/dist/{chunk-U647WKG5.js → chunk-L6BFDLLZ.js} +264 -48
- package/dist/chunk-L6BFDLLZ.js.map +1 -0
- package/dist/{chunk-6VAYVPEL.js → chunk-ZU4YUN33.js} +8 -8
- package/dist/chunk-ZU4YUN33.js.map +1 -0
- package/dist/index.js +41 -49812
- package/dist/index.js.map +1 -1
- package/dist/login-JWAG3GPR.js +18 -0
- package/dist/login-JWAG3GPR.js.map +1 -0
- package/dist/{logout-XX5ULFHB.js → logout-PIKY2YCJ.js} +7 -6
- package/dist/logout-PIKY2YCJ.js.map +1 -0
- package/dist/mcp-servers-2MAQ6WKL.js +15 -0
- package/dist/mcp-servers-2MAQ6WKL.js.map +1 -0
- package/dist/plugin-mcp-linear.d.ts +2 -0
- package/dist/plugin-mcp-linear.js +1683 -0
- package/dist/plugin-mcp-linear.js.map +1 -0
- package/dist/serve-2FNONIDL.js +69583 -0
- package/dist/serve-2FNONIDL.js.map +1 -0
- package/dist/skills-NCKYNLUS.js +9 -0
- package/dist/skills-NCKYNLUS.js.map +1 -0
- package/dist/start-CQC22BQF.js +35 -0
- package/dist/start-CQC22BQF.js.map +1 -0
- package/package.json +8 -3
- package/dist/chunk-6VAYVPEL.js.map +0 -1
- package/dist/chunk-FY3DRRGT.js.map +0 -1
- package/dist/chunk-HS57GMAL.js.map +0 -1
- package/dist/chunk-K3GFMEBF.js.map +0 -1
- package/dist/chunk-U647WKG5.js.map +0 -1
- package/dist/login-Q6PDH6HF.js.map +0 -1
- package/dist/logout-XX5ULFHB.js.map +0 -1
- /package/dist/{auth-XINVA3ZW.js.map → auth-V6KVU7VA.js.map} +0 -0
|
@@ -0,0 +1,1683 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../../packages/linear-plugin/dist/mcp.js
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
var LINEAR_API_URL = "https://api.linear.app/graphql";
|
|
8
|
+
function parseErrorBody(body) {
|
|
9
|
+
if (!body) return "";
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(
|
|
12
|
+
body
|
|
13
|
+
);
|
|
14
|
+
if (parsed.errors) {
|
|
15
|
+
return `: ${parsed.errors.map((e) => e.message).join("; ")}`;
|
|
16
|
+
}
|
|
17
|
+
if (parsed.error) {
|
|
18
|
+
return `: ${parsed.error}`;
|
|
19
|
+
}
|
|
20
|
+
return `: ${body}`;
|
|
21
|
+
} catch {
|
|
22
|
+
return `: ${body}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function readErrorResponse(response) {
|
|
26
|
+
const rateLimitReset = response.headers.get("X-RateLimit-Requests-Reset");
|
|
27
|
+
if (response.status === 429 && rateLimitReset) {
|
|
28
|
+
return `Linear API rate limited. Resets at ${rateLimitReset}. Please wait before retrying.`;
|
|
29
|
+
}
|
|
30
|
+
let body = "";
|
|
31
|
+
try {
|
|
32
|
+
body = await response.text();
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
const detail = parseErrorBody(body);
|
|
36
|
+
return `Linear API request failed (${response.status} ${response.statusText})${detail}`;
|
|
37
|
+
}
|
|
38
|
+
function stripUndefined(obj) {
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
41
|
+
if (value !== void 0) {
|
|
42
|
+
result[key] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
var LinearGraphQLClient = class {
|
|
48
|
+
#token;
|
|
49
|
+
constructor(token) {
|
|
50
|
+
this.#token = token;
|
|
51
|
+
}
|
|
52
|
+
async query(document, variables) {
|
|
53
|
+
const cleanedVariables = variables ? stripUndefined(variables) : void 0;
|
|
54
|
+
const response = await fetch(LINEAR_API_URL, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
Authorization: this.#token
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({ query: document, variables: cleanedVariables })
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(await readErrorResponse(response));
|
|
64
|
+
}
|
|
65
|
+
const json = await response.json();
|
|
66
|
+
if (json.errors && json.errors.length > 0) {
|
|
67
|
+
const messages = json.errors.map((e) => {
|
|
68
|
+
const ext = e.extensions ? ` (${JSON.stringify(e.extensions)})` : "";
|
|
69
|
+
return `${e.message}${ext}`;
|
|
70
|
+
}).join("; ");
|
|
71
|
+
throw new Error(`Linear GraphQL error: ${messages}`);
|
|
72
|
+
}
|
|
73
|
+
if (!json.data) {
|
|
74
|
+
throw new Error("Linear API returned no data");
|
|
75
|
+
}
|
|
76
|
+
return json.data;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
80
|
+
function isUUID(value) {
|
|
81
|
+
return UUID_RE.test(value);
|
|
82
|
+
}
|
|
83
|
+
var TEAM_KEY_RE = /^[A-Za-z]{1,10}$/;
|
|
84
|
+
async function resolveTeamId(client, teamIdOrKey) {
|
|
85
|
+
if (isUUID(teamIdOrKey)) return teamIdOrKey;
|
|
86
|
+
if (TEAM_KEY_RE.test(teamIdOrKey)) {
|
|
87
|
+
const data = await client.query(
|
|
88
|
+
`query($filter: TeamFilter, $first: Int) {
|
|
89
|
+
teams(filter: $filter, first: $first) {
|
|
90
|
+
nodes { id key }
|
|
91
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
92
|
+
}
|
|
93
|
+
}`,
|
|
94
|
+
{ filter: { key: { eq: teamIdOrKey.toUpperCase() } }, first: 1 }
|
|
95
|
+
);
|
|
96
|
+
const match = data.teams.nodes[0];
|
|
97
|
+
if (match) return match.id;
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Could not resolve team "${teamIdOrKey}". Use a UUID or team key like "ENG".`);
|
|
100
|
+
}
|
|
101
|
+
function formatConnection(connection, formatNode) {
|
|
102
|
+
const lines = connection.nodes.map(formatNode);
|
|
103
|
+
const pagination = [];
|
|
104
|
+
if (connection.pageInfo.hasNextPage) {
|
|
105
|
+
pagination.push(`Next page cursor: ${connection.pageInfo.endCursor}`);
|
|
106
|
+
}
|
|
107
|
+
if (connection.pageInfo.hasPreviousPage) {
|
|
108
|
+
pagination.push(`Previous page cursor: ${connection.pageInfo.startCursor}`);
|
|
109
|
+
}
|
|
110
|
+
if (pagination.length > 0) {
|
|
111
|
+
lines.push("", "---", ...pagination);
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
var ISSUE_IDENTIFIER_RE = /^[A-Za-z]+-\d+$/;
|
|
116
|
+
async function resolveIssueIdForComments(client, idOrIdentifier) {
|
|
117
|
+
if (isUUID(idOrIdentifier)) return idOrIdentifier;
|
|
118
|
+
if (ISSUE_IDENTIFIER_RE.test(idOrIdentifier)) {
|
|
119
|
+
const data = await client.query(
|
|
120
|
+
`query($term: String!, $first: Int) {
|
|
121
|
+
searchIssues(term: $term, first: $first) {
|
|
122
|
+
nodes { id identifier }
|
|
123
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
124
|
+
}
|
|
125
|
+
}`,
|
|
126
|
+
{ term: idOrIdentifier.toUpperCase(), first: 5 }
|
|
127
|
+
);
|
|
128
|
+
const match = data.searchIssues.nodes.find(
|
|
129
|
+
(n) => n.identifier.toUpperCase() === idOrIdentifier.toUpperCase()
|
|
130
|
+
);
|
|
131
|
+
if (match) return match.id;
|
|
132
|
+
}
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Could not resolve issue "${idOrIdentifier}". Use a UUID or identifier like "ENG-123".`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
async function listComments(client, params) {
|
|
138
|
+
const issueId = await resolveIssueIdForComments(client, params.issueId);
|
|
139
|
+
const data = await client.query(
|
|
140
|
+
`query($id: String!) {
|
|
141
|
+
issue(id: $id) {
|
|
142
|
+
identifier title
|
|
143
|
+
comments {
|
|
144
|
+
nodes {
|
|
145
|
+
id body createdAt updatedAt
|
|
146
|
+
user { id name email }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}`,
|
|
151
|
+
{ id: issueId }
|
|
152
|
+
);
|
|
153
|
+
const { issue } = data;
|
|
154
|
+
if (issue.comments.nodes.length === 0) {
|
|
155
|
+
return `No comments on ${issue.identifier}: ${issue.title}`;
|
|
156
|
+
}
|
|
157
|
+
const lines = [`# Comments on ${issue.identifier}: ${issue.title}`, ""];
|
|
158
|
+
for (const comment of issue.comments.nodes) {
|
|
159
|
+
const author = comment.user ? comment.user.name : "Unknown";
|
|
160
|
+
lines.push(`**${author}** (${comment.createdAt}) [ID: ${comment.id}]:`);
|
|
161
|
+
lines.push(comment.body, "");
|
|
162
|
+
}
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
async function createComment(client, params) {
|
|
166
|
+
const issueId = await resolveIssueIdForComments(client, params.issueId);
|
|
167
|
+
const data = await client.query(
|
|
168
|
+
`mutation($input: CommentCreateInput!) {
|
|
169
|
+
commentCreate(input: $input) {
|
|
170
|
+
success
|
|
171
|
+
comment {
|
|
172
|
+
id body
|
|
173
|
+
issue { identifier }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}`,
|
|
177
|
+
{ input: { issueId, body: params.body } }
|
|
178
|
+
);
|
|
179
|
+
const comment = data.commentCreate.comment;
|
|
180
|
+
return `Comment added to ${comment.issue.identifier}
|
|
181
|
+
ID: ${comment.id}`;
|
|
182
|
+
}
|
|
183
|
+
async function deleteComment(client, params) {
|
|
184
|
+
const data = await client.query(
|
|
185
|
+
`mutation($id: String!) {
|
|
186
|
+
commentDelete(id: $id) { success }
|
|
187
|
+
}`,
|
|
188
|
+
{ id: params.id }
|
|
189
|
+
);
|
|
190
|
+
if (!data.commentDelete.success) {
|
|
191
|
+
throw new Error(`Failed to delete comment "${params.id}".`);
|
|
192
|
+
}
|
|
193
|
+
return `Deleted comment ${params.id}.`;
|
|
194
|
+
}
|
|
195
|
+
var DOCUMENT_FIELDS = `
|
|
196
|
+
id title icon color url createdAt updatedAt
|
|
197
|
+
creator { id name }
|
|
198
|
+
project { id name }
|
|
199
|
+
`;
|
|
200
|
+
function formatDocSummary(doc) {
|
|
201
|
+
const creator = doc.creator ? ` | By: ${doc.creator.name}` : "";
|
|
202
|
+
const project = doc.project ? ` | Project: ${doc.project.name}` : "";
|
|
203
|
+
return `${doc.title}${creator}${project}
|
|
204
|
+
URL: ${doc.url} | ID: ${doc.id}`;
|
|
205
|
+
}
|
|
206
|
+
async function getDocument(client, params) {
|
|
207
|
+
const data = await client.query(
|
|
208
|
+
`query($id: String!) {
|
|
209
|
+
document(id: $id) {
|
|
210
|
+
${DOCUMENT_FIELDS}
|
|
211
|
+
content
|
|
212
|
+
}
|
|
213
|
+
}`,
|
|
214
|
+
{ id: params.id }
|
|
215
|
+
);
|
|
216
|
+
const doc = data.document;
|
|
217
|
+
const lines = [`# ${doc.title}`, "", `**URL:** ${doc.url}`, `**ID:** ${doc.id}`];
|
|
218
|
+
if (doc.creator) lines.push(`**Creator:** ${doc.creator.name}`);
|
|
219
|
+
if (doc.project) lines.push(`**Project:** ${doc.project.name}`);
|
|
220
|
+
lines.push(`**Created:** ${doc.createdAt}`, `**Updated:** ${doc.updatedAt}`);
|
|
221
|
+
if (doc.content) {
|
|
222
|
+
lines.push("", "---", "", doc.content);
|
|
223
|
+
}
|
|
224
|
+
return lines.join("\n");
|
|
225
|
+
}
|
|
226
|
+
var VALID_PAGINATION_ORDER_BY = /* @__PURE__ */ new Set(["createdAt", "updatedAt"]);
|
|
227
|
+
function normalizeOrderBy(orderBy) {
|
|
228
|
+
if (!orderBy) return void 0;
|
|
229
|
+
if (VALID_PAGINATION_ORDER_BY.has(orderBy)) return orderBy;
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
async function listDocuments(client, params) {
|
|
233
|
+
const limit = params.limit ?? 25;
|
|
234
|
+
const filter = {};
|
|
235
|
+
if (params.query) filter.title = { containsIgnoreCase: params.query };
|
|
236
|
+
if (params.projectId) filter.project = { id: { eq: params.projectId } };
|
|
237
|
+
if (params.creatorId) filter.creator = { id: { eq: params.creatorId } };
|
|
238
|
+
if (params.createdAt) filter.createdAt = { gte: params.createdAt };
|
|
239
|
+
if (params.updatedAt) filter.updatedAt = { gte: params.updatedAt };
|
|
240
|
+
const data = await client.query(
|
|
241
|
+
`query($filter: DocumentFilter, $first: Int, $after: String, $before: String, $includeArchived: Boolean, $orderBy: PaginationOrderBy) {
|
|
242
|
+
documents(filter: $filter, first: $first, after: $after, before: $before, includeArchived: $includeArchived, orderBy: $orderBy) {
|
|
243
|
+
nodes { ${DOCUMENT_FIELDS} }
|
|
244
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
245
|
+
}
|
|
246
|
+
}`,
|
|
247
|
+
{
|
|
248
|
+
filter: Object.keys(filter).length > 0 ? filter : void 0,
|
|
249
|
+
first: limit,
|
|
250
|
+
after: params.after,
|
|
251
|
+
before: params.before,
|
|
252
|
+
includeArchived: params.includeArchived,
|
|
253
|
+
orderBy: normalizeOrderBy(params.orderBy)
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
if (data.documents.nodes.length === 0) {
|
|
257
|
+
return "No documents found.";
|
|
258
|
+
}
|
|
259
|
+
return formatConnection(data.documents, formatDocSummary);
|
|
260
|
+
}
|
|
261
|
+
async function searchDocumentation(_client, params) {
|
|
262
|
+
const page = params.page ?? 1;
|
|
263
|
+
const response = await fetch(
|
|
264
|
+
`https://linear.app/docs/api/search?q=${encodeURIComponent(params.query)}&page=${page}`
|
|
265
|
+
);
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
return `Linear documentation search is not available via this server. Use https://linear.app/docs and search for: ${params.query}`;
|
|
268
|
+
}
|
|
269
|
+
const text = await response.text();
|
|
270
|
+
return text || `No documentation results found for: ${params.query}`;
|
|
271
|
+
}
|
|
272
|
+
var ISSUE_IDENTIFIER_RE2 = /^[A-Za-z]+-\d+$/;
|
|
273
|
+
var ISSUE_FIELDS = `
|
|
274
|
+
id identifier title description priority priorityLabel url branchName
|
|
275
|
+
createdAt updatedAt dueDate estimate
|
|
276
|
+
state { id name type }
|
|
277
|
+
team { id key name }
|
|
278
|
+
assignee { id name email }
|
|
279
|
+
project { id name }
|
|
280
|
+
parent { id identifier title }
|
|
281
|
+
labels { nodes { id name } }
|
|
282
|
+
`;
|
|
283
|
+
var ISSUE_DETAIL_FIELDS = `
|
|
284
|
+
${ISSUE_FIELDS}
|
|
285
|
+
attachments { nodes { id title url } }
|
|
286
|
+
comments { nodes { id body createdAt user { name } } }
|
|
287
|
+
`;
|
|
288
|
+
var ISSUE_LIST_FIELDS = `
|
|
289
|
+
id identifier title priority priorityLabel url
|
|
290
|
+
createdAt updatedAt dueDate
|
|
291
|
+
state { id name type }
|
|
292
|
+
team { id key name }
|
|
293
|
+
assignee { id name email }
|
|
294
|
+
project { id name }
|
|
295
|
+
labels { nodes { id name } }
|
|
296
|
+
`;
|
|
297
|
+
function formatIssueSummary(issue) {
|
|
298
|
+
const assignee = issue.assignee ? ` | Assignee: ${issue.assignee.name}` : "";
|
|
299
|
+
const labels = issue.labels.nodes.length > 0 ? ` | Labels: ${issue.labels.nodes.map((l) => l.name).join(", ")}` : "";
|
|
300
|
+
return `[${issue.identifier}] ${issue.title} (${issue.state.name}, ${issue.priorityLabel})${assignee}${labels}
|
|
301
|
+
URL: ${issue.url}`;
|
|
302
|
+
}
|
|
303
|
+
function formatIssueMetadata(issue) {
|
|
304
|
+
const lines = [
|
|
305
|
+
`# ${issue.identifier}: ${issue.title}`,
|
|
306
|
+
"",
|
|
307
|
+
`**Status:** ${issue.state.name} (${issue.state.type})`,
|
|
308
|
+
`**Priority:** ${issue.priorityLabel}`,
|
|
309
|
+
`**Team:** ${issue.team.name} (${issue.team.key})`
|
|
310
|
+
];
|
|
311
|
+
if (issue.assignee) lines.push(`**Assignee:** ${issue.assignee.name} (${issue.assignee.email})`);
|
|
312
|
+
if (issue.project) lines.push(`**Project:** ${issue.project.name}`);
|
|
313
|
+
if (issue.parent) lines.push(`**Parent:** ${issue.parent.identifier} - ${issue.parent.title}`);
|
|
314
|
+
if (issue.dueDate) lines.push(`**Due:** ${issue.dueDate}`);
|
|
315
|
+
if (issue.estimate !== void 0) lines.push(`**Estimate:** ${issue.estimate}`);
|
|
316
|
+
if (issue.labels.nodes.length > 0) {
|
|
317
|
+
lines.push(`**Labels:** ${issue.labels.nodes.map((l) => l.name).join(", ")}`);
|
|
318
|
+
}
|
|
319
|
+
lines.push(`**URL:** ${issue.url}`, `**ID:** ${issue.id}`);
|
|
320
|
+
return lines;
|
|
321
|
+
}
|
|
322
|
+
function formatIssueDetail(issue) {
|
|
323
|
+
const lines = formatIssueMetadata(issue);
|
|
324
|
+
if (issue.description) {
|
|
325
|
+
lines.push("", "## Description", "", issue.description);
|
|
326
|
+
}
|
|
327
|
+
if (issue.attachments.nodes.length > 0) {
|
|
328
|
+
lines.push("", "## Attachments");
|
|
329
|
+
for (const a of issue.attachments.nodes) {
|
|
330
|
+
lines.push(`- [${a.title}](${a.url})`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (issue.comments.nodes.length > 0) {
|
|
334
|
+
lines.push("", "## Comments");
|
|
335
|
+
for (const c of issue.comments.nodes) {
|
|
336
|
+
const author = c.user ? c.user.name : "Unknown";
|
|
337
|
+
lines.push("", `**${author}** (${c.createdAt}) [ID: ${c.id}]:`, c.body);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return lines.join("\n");
|
|
341
|
+
}
|
|
342
|
+
async function resolveAssigneeId(client, assigneeId) {
|
|
343
|
+
if (assigneeId === "me") {
|
|
344
|
+
const viewer = await client.query(`query { viewer { id } }`);
|
|
345
|
+
return viewer.viewer.id;
|
|
346
|
+
}
|
|
347
|
+
return assigneeId;
|
|
348
|
+
}
|
|
349
|
+
var VALID_PAGINATION_ORDER_BY2 = /* @__PURE__ */ new Set(["createdAt", "updatedAt"]);
|
|
350
|
+
function normalizeOrderBy2(orderBy) {
|
|
351
|
+
if (!orderBy) return void 0;
|
|
352
|
+
if (VALID_PAGINATION_ORDER_BY2.has(orderBy)) return orderBy;
|
|
353
|
+
return void 0;
|
|
354
|
+
}
|
|
355
|
+
async function listIssues(client, params) {
|
|
356
|
+
const limit = Math.min(params.limit ?? 25, 100);
|
|
357
|
+
const validOrderBy = normalizeOrderBy2(params.orderBy);
|
|
358
|
+
const [teamId, assigneeId] = await Promise.all([
|
|
359
|
+
params.teamId ? resolveTeamId(client, params.teamId) : Promise.resolve(void 0),
|
|
360
|
+
resolveAssigneeId(client, params.assigneeId)
|
|
361
|
+
]);
|
|
362
|
+
if (params.query) {
|
|
363
|
+
const data2 = await client.query(
|
|
364
|
+
`query($term: String!, $first: Int, $includeArchived: Boolean) {
|
|
365
|
+
searchIssues(term: $term, first: $first, includeArchived: $includeArchived) {
|
|
366
|
+
nodes { ${ISSUE_LIST_FIELDS} }
|
|
367
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
368
|
+
}
|
|
369
|
+
}`,
|
|
370
|
+
{
|
|
371
|
+
term: params.query,
|
|
372
|
+
first: limit,
|
|
373
|
+
includeArchived: params.includeArchived
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
return formatConnection(data2.searchIssues, formatIssueSummary);
|
|
377
|
+
}
|
|
378
|
+
const filter = buildIssueFilter({ ...params, teamId, assigneeId });
|
|
379
|
+
const data = await client.query(
|
|
380
|
+
`query($filter: IssueFilter, $first: Int, $after: String, $includeArchived: Boolean, $orderBy: PaginationOrderBy) {
|
|
381
|
+
issues(filter: $filter, first: $first, after: $after, includeArchived: $includeArchived, orderBy: $orderBy) {
|
|
382
|
+
nodes { ${ISSUE_LIST_FIELDS} }
|
|
383
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
384
|
+
}
|
|
385
|
+
}`,
|
|
386
|
+
{
|
|
387
|
+
filter: Object.keys(filter).length > 0 ? filter : void 0,
|
|
388
|
+
first: limit,
|
|
389
|
+
after: params.after,
|
|
390
|
+
includeArchived: params.includeArchived,
|
|
391
|
+
orderBy: validOrderBy
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
return formatConnection(data.issues, formatIssueSummary);
|
|
395
|
+
}
|
|
396
|
+
function buildIssueFilter(params) {
|
|
397
|
+
const filter = {};
|
|
398
|
+
if (params.teamId) filter.team = { id: { eq: params.teamId } };
|
|
399
|
+
if (params.stateId) filter.state = { id: { eq: params.stateId } };
|
|
400
|
+
if (params.assigneeId) filter.assignee = { id: { eq: params.assigneeId } };
|
|
401
|
+
if (params.parentId) filter.parent = { id: { eq: params.parentId } };
|
|
402
|
+
if (params.projectId) filter.project = { id: { eq: params.projectId } };
|
|
403
|
+
if (params.createdAt) filter.createdAt = { gte: params.createdAt };
|
|
404
|
+
if (params.updatedAt) filter.updatedAt = { gte: params.updatedAt };
|
|
405
|
+
return filter;
|
|
406
|
+
}
|
|
407
|
+
async function getIssue(client, params) {
|
|
408
|
+
if (isUUID(params.id)) {
|
|
409
|
+
const data = await client.query(
|
|
410
|
+
`query($id: String!) {
|
|
411
|
+
issue(id: $id) { ${ISSUE_DETAIL_FIELDS} }
|
|
412
|
+
}`,
|
|
413
|
+
{ id: params.id }
|
|
414
|
+
);
|
|
415
|
+
return formatIssueDetail(data.issue);
|
|
416
|
+
}
|
|
417
|
+
if (ISSUE_IDENTIFIER_RE2.test(params.id)) {
|
|
418
|
+
const searchData2 = await client.query(
|
|
419
|
+
`query($term: String!, $first: Int) {
|
|
420
|
+
searchIssues(term: $term, first: $first) {
|
|
421
|
+
nodes { ${ISSUE_DETAIL_FIELDS} }
|
|
422
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
423
|
+
}
|
|
424
|
+
}`,
|
|
425
|
+
{ term: params.id.toUpperCase(), first: 5 }
|
|
426
|
+
);
|
|
427
|
+
const match = searchData2.searchIssues.nodes.find(
|
|
428
|
+
(n) => n.identifier.toUpperCase() === params.id.toUpperCase()
|
|
429
|
+
);
|
|
430
|
+
if (match) {
|
|
431
|
+
return formatIssueDetail(match);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const searchData = await client.query(
|
|
435
|
+
`query($term: String!, $first: Int) {
|
|
436
|
+
searchIssues(term: $term, first: $first) {
|
|
437
|
+
nodes { ${ISSUE_DETAIL_FIELDS} }
|
|
438
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
439
|
+
}
|
|
440
|
+
}`,
|
|
441
|
+
{ term: params.id, first: 1 }
|
|
442
|
+
);
|
|
443
|
+
const first = searchData.searchIssues.nodes[0];
|
|
444
|
+
if (!first) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`Issue not found: "${params.id}". Try using a UUID, an identifier like "ENG-123", or a search term.`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return formatIssueDetail(first);
|
|
450
|
+
}
|
|
451
|
+
async function createIssue(client, params) {
|
|
452
|
+
const [teamId, assigneeId] = await Promise.all([
|
|
453
|
+
resolveTeamId(client, params.teamId),
|
|
454
|
+
resolveAssigneeId(client, params.assigneeId)
|
|
455
|
+
]);
|
|
456
|
+
const input = {
|
|
457
|
+
title: params.title,
|
|
458
|
+
teamId
|
|
459
|
+
};
|
|
460
|
+
if (params.description !== void 0) input.description = params.description;
|
|
461
|
+
if (params.priority !== void 0) input.priority = params.priority;
|
|
462
|
+
if (params.projectId) input.projectId = params.projectId;
|
|
463
|
+
if (params.parentId) input.parentId = params.parentId;
|
|
464
|
+
if (params.stateId) input.stateId = params.stateId;
|
|
465
|
+
if (assigneeId) input.assigneeId = assigneeId;
|
|
466
|
+
if (params.labelIds) input.labelIds = params.labelIds;
|
|
467
|
+
if (params.dueDate) input.dueDate = params.dueDate;
|
|
468
|
+
const data = await client.query(
|
|
469
|
+
`mutation($input: IssueCreateInput!) {
|
|
470
|
+
issueCreate(input: $input) {
|
|
471
|
+
success
|
|
472
|
+
issue {
|
|
473
|
+
id identifier title url
|
|
474
|
+
state { name }
|
|
475
|
+
team { key }
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}`,
|
|
479
|
+
{ input }
|
|
480
|
+
);
|
|
481
|
+
const issue = data.issueCreate.issue;
|
|
482
|
+
return `Created issue ${issue.identifier}: ${issue.title}
|
|
483
|
+
Status: ${issue.state.name}
|
|
484
|
+
URL: ${issue.url}
|
|
485
|
+
ID: ${issue.id}`;
|
|
486
|
+
}
|
|
487
|
+
async function updateIssue(client, params) {
|
|
488
|
+
const resolvedId = await resolveIssueId(client, params.id);
|
|
489
|
+
const resolvedAssignee = await resolveAssigneeId(client, params.assigneeId);
|
|
490
|
+
const { id: _id, assigneeId: _assigneeId, ...rest } = params;
|
|
491
|
+
const input = {};
|
|
492
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
493
|
+
if (value !== void 0) {
|
|
494
|
+
input[key] = value;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (resolvedAssignee !== void 0) input.assigneeId = resolvedAssignee;
|
|
498
|
+
const data = await client.query(
|
|
499
|
+
`mutation($id: String!, $input: IssueUpdateInput!) {
|
|
500
|
+
issueUpdate(id: $id, input: $input) {
|
|
501
|
+
success
|
|
502
|
+
issue {
|
|
503
|
+
id identifier title url
|
|
504
|
+
state { name }
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}`,
|
|
508
|
+
{ id: resolvedId, input }
|
|
509
|
+
);
|
|
510
|
+
const issue = data.issueUpdate.issue;
|
|
511
|
+
return `Updated issue ${issue.identifier}: ${issue.title}
|
|
512
|
+
Status: ${issue.state.name}
|
|
513
|
+
URL: ${issue.url}`;
|
|
514
|
+
}
|
|
515
|
+
async function deleteIssue(client, params) {
|
|
516
|
+
const resolvedId = await resolveIssueId(client, params.id);
|
|
517
|
+
const data = await client.query(
|
|
518
|
+
`mutation($id: String!) {
|
|
519
|
+
issueDelete(id: $id) { success }
|
|
520
|
+
}`,
|
|
521
|
+
{ id: resolvedId }
|
|
522
|
+
);
|
|
523
|
+
if (!data.issueDelete.success) {
|
|
524
|
+
throw new Error(`Failed to delete issue "${params.id}".`);
|
|
525
|
+
}
|
|
526
|
+
return `Deleted issue ${params.id}.`;
|
|
527
|
+
}
|
|
528
|
+
async function listMyIssues(client, params) {
|
|
529
|
+
const limit = params.limit ?? 25;
|
|
530
|
+
const validOrderBy = normalizeOrderBy2(params.orderBy);
|
|
531
|
+
const data = await client.query(
|
|
532
|
+
`query($first: Int, $after: String, $before: String, $orderBy: PaginationOrderBy) {
|
|
533
|
+
viewer {
|
|
534
|
+
assignedIssues(first: $first, after: $after, before: $before, orderBy: $orderBy) {
|
|
535
|
+
nodes { ${ISSUE_LIST_FIELDS} }
|
|
536
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}`,
|
|
540
|
+
{
|
|
541
|
+
first: limit,
|
|
542
|
+
after: params.after,
|
|
543
|
+
before: params.before,
|
|
544
|
+
orderBy: validOrderBy
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
if (data.viewer.assignedIssues.nodes.length === 0) {
|
|
548
|
+
return "No issues assigned to you.";
|
|
549
|
+
}
|
|
550
|
+
return formatConnection(data.viewer.assignedIssues, formatIssueSummary);
|
|
551
|
+
}
|
|
552
|
+
async function getIssueGitBranchName(client, params) {
|
|
553
|
+
const issueId = await resolveIssueId(client, params.id);
|
|
554
|
+
const data = await client.query(
|
|
555
|
+
`query($id: String!) {
|
|
556
|
+
issue(id: $id) { identifier branchName }
|
|
557
|
+
}`,
|
|
558
|
+
{ id: issueId }
|
|
559
|
+
);
|
|
560
|
+
return `Branch name for ${data.issue.identifier}: ${data.issue.branchName}`;
|
|
561
|
+
}
|
|
562
|
+
async function resolveIssueId(client, idOrIdentifier) {
|
|
563
|
+
if (isUUID(idOrIdentifier)) return idOrIdentifier;
|
|
564
|
+
if (ISSUE_IDENTIFIER_RE2.test(idOrIdentifier)) {
|
|
565
|
+
const data = await client.query(
|
|
566
|
+
`query($term: String!, $first: Int) {
|
|
567
|
+
searchIssues(term: $term, first: $first) {
|
|
568
|
+
nodes { id identifier }
|
|
569
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
570
|
+
}
|
|
571
|
+
}`,
|
|
572
|
+
{ term: idOrIdentifier.toUpperCase(), first: 5 }
|
|
573
|
+
);
|
|
574
|
+
const match = data.searchIssues.nodes.find(
|
|
575
|
+
(n) => n.identifier.toUpperCase() === idOrIdentifier.toUpperCase()
|
|
576
|
+
);
|
|
577
|
+
if (match) return match.id;
|
|
578
|
+
}
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Could not resolve issue "${idOrIdentifier}". Use a UUID or identifier like "ENG-123".`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
var PROJECT_FIELDS = `
|
|
584
|
+
id name description url state progress
|
|
585
|
+
startDate targetDate createdAt updatedAt
|
|
586
|
+
lead { id name }
|
|
587
|
+
teams { nodes { id key name } }
|
|
588
|
+
`;
|
|
589
|
+
function formatProjectSummary(project) {
|
|
590
|
+
const lead = project.lead ? ` | Lead: ${project.lead.name}` : "";
|
|
591
|
+
const teams = project.teams.nodes.map((t) => t.key).join(", ");
|
|
592
|
+
return `${project.name} (${project.state}, ${Math.round(project.progress * 100)}%)${lead}
|
|
593
|
+
Teams: ${teams} | URL: ${project.url}
|
|
594
|
+
ID: ${project.id}`;
|
|
595
|
+
}
|
|
596
|
+
function formatProjectDetail(project) {
|
|
597
|
+
const lines = [
|
|
598
|
+
`# ${project.name}`,
|
|
599
|
+
"",
|
|
600
|
+
`**State:** ${project.state}`,
|
|
601
|
+
`**Progress:** ${Math.round(project.progress * 100)}%`,
|
|
602
|
+
`**Teams:** ${project.teams.nodes.map((t) => `${t.name} (${t.key})`).join(", ")}`
|
|
603
|
+
];
|
|
604
|
+
if (project.lead) lines.push(`**Lead:** ${project.lead.name}`);
|
|
605
|
+
if (project.startDate) lines.push(`**Start Date:** ${project.startDate}`);
|
|
606
|
+
if (project.targetDate) lines.push(`**Target Date:** ${project.targetDate}`);
|
|
607
|
+
lines.push(`**URL:** ${project.url}`, `**ID:** ${project.id}`);
|
|
608
|
+
if (project.description) {
|
|
609
|
+
lines.push("", "## Description", "", project.description);
|
|
610
|
+
}
|
|
611
|
+
return lines.join("\n");
|
|
612
|
+
}
|
|
613
|
+
var VALID_PAGINATION_ORDER_BY3 = /* @__PURE__ */ new Set(["createdAt", "updatedAt"]);
|
|
614
|
+
function normalizeOrderBy3(orderBy) {
|
|
615
|
+
if (!orderBy) return void 0;
|
|
616
|
+
if (VALID_PAGINATION_ORDER_BY3.has(orderBy)) return orderBy;
|
|
617
|
+
return void 0;
|
|
618
|
+
}
|
|
619
|
+
async function listProjects(client, params) {
|
|
620
|
+
const limit = params.limit ?? 25;
|
|
621
|
+
const validOrderBy = normalizeOrderBy3(params.orderBy);
|
|
622
|
+
if (params.teamId) {
|
|
623
|
+
const teamId = await resolveTeamId(client, params.teamId);
|
|
624
|
+
const data2 = await client.query(
|
|
625
|
+
`query($teamId: String!, $first: Int, $after: String, $before: String, $orderBy: PaginationOrderBy, $filter: ProjectFilter) {
|
|
626
|
+
team(id: $teamId) {
|
|
627
|
+
projects(first: $first, after: $after, before: $before, orderBy: $orderBy, filter: $filter) {
|
|
628
|
+
nodes { ${PROJECT_FIELDS} }
|
|
629
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}`,
|
|
633
|
+
{
|
|
634
|
+
teamId,
|
|
635
|
+
first: limit,
|
|
636
|
+
after: params.after,
|
|
637
|
+
before: params.before,
|
|
638
|
+
orderBy: validOrderBy,
|
|
639
|
+
filter: buildProjectFilter(params)
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
if (data2.team.projects.nodes.length === 0) {
|
|
643
|
+
return "No projects found for this team.";
|
|
644
|
+
}
|
|
645
|
+
return formatConnection(data2.team.projects, formatProjectSummary);
|
|
646
|
+
}
|
|
647
|
+
const filter = buildProjectFilter(params);
|
|
648
|
+
const data = await client.query(
|
|
649
|
+
`query($filter: ProjectFilter, $first: Int, $after: String, $before: String, $includeArchived: Boolean, $orderBy: PaginationOrderBy) {
|
|
650
|
+
projects(filter: $filter, first: $first, after: $after, before: $before, includeArchived: $includeArchived, orderBy: $orderBy) {
|
|
651
|
+
nodes { ${PROJECT_FIELDS} }
|
|
652
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
653
|
+
}
|
|
654
|
+
}`,
|
|
655
|
+
{
|
|
656
|
+
filter,
|
|
657
|
+
first: limit,
|
|
658
|
+
after: params.after,
|
|
659
|
+
before: params.before,
|
|
660
|
+
includeArchived: params.includeArchived,
|
|
661
|
+
orderBy: validOrderBy
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
if (data.projects.nodes.length === 0) {
|
|
665
|
+
return "No projects found.";
|
|
666
|
+
}
|
|
667
|
+
return formatConnection(data.projects, formatProjectSummary);
|
|
668
|
+
}
|
|
669
|
+
function buildProjectFilter(params) {
|
|
670
|
+
const filter = {};
|
|
671
|
+
if (params.createdAt) filter.createdAt = { gte: params.createdAt };
|
|
672
|
+
if (params.updatedAt) filter.updatedAt = { gte: params.updatedAt };
|
|
673
|
+
return Object.keys(filter).length > 0 ? filter : void 0;
|
|
674
|
+
}
|
|
675
|
+
async function getProject(client, params) {
|
|
676
|
+
if (isUUID(params.query)) {
|
|
677
|
+
const data2 = await client.query(
|
|
678
|
+
`query($id: String!) {
|
|
679
|
+
project(id: $id) { ${PROJECT_FIELDS} }
|
|
680
|
+
}`,
|
|
681
|
+
{ id: params.query }
|
|
682
|
+
);
|
|
683
|
+
return formatProjectDetail(data2.project);
|
|
684
|
+
}
|
|
685
|
+
const data = await client.query(
|
|
686
|
+
`query($name: String!) {
|
|
687
|
+
projects(filter: { name: { containsIgnoreCase: $name } }, first: 5) {
|
|
688
|
+
nodes { ${PROJECT_FIELDS} }
|
|
689
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
690
|
+
}
|
|
691
|
+
}`,
|
|
692
|
+
{ name: params.query }
|
|
693
|
+
);
|
|
694
|
+
if (data.projects.nodes.length === 0) {
|
|
695
|
+
throw new Error(`No project matching "${params.query}" found.`);
|
|
696
|
+
}
|
|
697
|
+
if (data.projects.nodes.length === 1) {
|
|
698
|
+
const first = data.projects.nodes[0];
|
|
699
|
+
if (!first) throw new Error(`No project matching "${params.query}" found.`);
|
|
700
|
+
return formatProjectDetail(first);
|
|
701
|
+
}
|
|
702
|
+
return formatConnection(data.projects, formatProjectSummary);
|
|
703
|
+
}
|
|
704
|
+
async function createProject(client, params) {
|
|
705
|
+
const teamId = await resolveTeamId(client, params.teamId);
|
|
706
|
+
const input = {
|
|
707
|
+
name: params.name,
|
|
708
|
+
teamIds: [teamId]
|
|
709
|
+
};
|
|
710
|
+
if (params.summary) input.summary = params.summary;
|
|
711
|
+
if (params.description) input.description = params.description;
|
|
712
|
+
if (params.startDate) input.startDate = params.startDate;
|
|
713
|
+
if (params.targetDate) input.targetDate = params.targetDate;
|
|
714
|
+
const data = await client.query(
|
|
715
|
+
`mutation($input: ProjectCreateInput!) {
|
|
716
|
+
projectCreate(input: $input) {
|
|
717
|
+
success
|
|
718
|
+
project { id name url state }
|
|
719
|
+
}
|
|
720
|
+
}`,
|
|
721
|
+
{ input }
|
|
722
|
+
);
|
|
723
|
+
const project = data.projectCreate.project;
|
|
724
|
+
return `Created project: ${project.name}
|
|
725
|
+
State: ${project.state}
|
|
726
|
+
URL: ${project.url}
|
|
727
|
+
ID: ${project.id}`;
|
|
728
|
+
}
|
|
729
|
+
async function updateProject(client, params) {
|
|
730
|
+
const { id, ...rest } = params;
|
|
731
|
+
const input = {};
|
|
732
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
733
|
+
if (value !== void 0) {
|
|
734
|
+
input[key] = value;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const data = await client.query(
|
|
738
|
+
`mutation($id: String!, $input: ProjectUpdateInput!) {
|
|
739
|
+
projectUpdate(id: $id, input: $input) {
|
|
740
|
+
success
|
|
741
|
+
project { id name url state }
|
|
742
|
+
}
|
|
743
|
+
}`,
|
|
744
|
+
{ id, input }
|
|
745
|
+
);
|
|
746
|
+
const project = data.projectUpdate.project;
|
|
747
|
+
return `Updated project: ${project.name}
|
|
748
|
+
State: ${project.state}
|
|
749
|
+
URL: ${project.url}`;
|
|
750
|
+
}
|
|
751
|
+
async function deleteProject(client, params) {
|
|
752
|
+
const data = await client.query(
|
|
753
|
+
`mutation($id: String!) {
|
|
754
|
+
projectDelete(id: $id) { success }
|
|
755
|
+
}`,
|
|
756
|
+
{ id: params.id }
|
|
757
|
+
);
|
|
758
|
+
if (!data.projectDelete.success) {
|
|
759
|
+
throw new Error(`Failed to delete project "${params.id}".`);
|
|
760
|
+
}
|
|
761
|
+
return `Deleted project ${params.id}.`;
|
|
762
|
+
}
|
|
763
|
+
var TEAM_FIELDS = `
|
|
764
|
+
id name key description icon color private timezone issueCount
|
|
765
|
+
`;
|
|
766
|
+
function formatTeamSummary(team) {
|
|
767
|
+
const priv = team.private ? " (private)" : "";
|
|
768
|
+
return `${team.name} [${team.key}]${priv} | ${team.issueCount} issues
|
|
769
|
+
ID: ${team.id}`;
|
|
770
|
+
}
|
|
771
|
+
function formatTeamDetail(team) {
|
|
772
|
+
const lines = [
|
|
773
|
+
`# ${team.name} [${team.key}]`,
|
|
774
|
+
"",
|
|
775
|
+
`**ID:** ${team.id}`,
|
|
776
|
+
`**Private:** ${team.private}`,
|
|
777
|
+
`**Issues:** ${team.issueCount}`
|
|
778
|
+
];
|
|
779
|
+
if (team.description) lines.push(`**Description:** ${team.description}`);
|
|
780
|
+
if (team.timezone) lines.push(`**Timezone:** ${team.timezone}`);
|
|
781
|
+
if (team.color) lines.push(`**Color:** ${team.color}`);
|
|
782
|
+
return lines.join("\n");
|
|
783
|
+
}
|
|
784
|
+
var VALID_PAGINATION_ORDER_BY4 = /* @__PURE__ */ new Set(["createdAt", "updatedAt"]);
|
|
785
|
+
function normalizeOrderBy4(orderBy) {
|
|
786
|
+
if (!orderBy) return void 0;
|
|
787
|
+
if (VALID_PAGINATION_ORDER_BY4.has(orderBy)) return orderBy;
|
|
788
|
+
return void 0;
|
|
789
|
+
}
|
|
790
|
+
async function listTeams(client, params) {
|
|
791
|
+
const limit = params.limit ?? 50;
|
|
792
|
+
const filter = {};
|
|
793
|
+
if (params.query) filter.name = { containsIgnoreCase: params.query };
|
|
794
|
+
if (params.createdAt) filter.createdAt = { gte: params.createdAt };
|
|
795
|
+
if (params.updatedAt) filter.updatedAt = { gte: params.updatedAt };
|
|
796
|
+
const data = await client.query(
|
|
797
|
+
`query($filter: TeamFilter, $first: Int, $after: String, $before: String, $includeArchived: Boolean, $orderBy: PaginationOrderBy) {
|
|
798
|
+
teams(filter: $filter, first: $first, after: $after, before: $before, includeArchived: $includeArchived, orderBy: $orderBy) {
|
|
799
|
+
nodes { ${TEAM_FIELDS} }
|
|
800
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
801
|
+
}
|
|
802
|
+
}`,
|
|
803
|
+
{
|
|
804
|
+
filter: Object.keys(filter).length > 0 ? filter : void 0,
|
|
805
|
+
first: limit,
|
|
806
|
+
after: params.after,
|
|
807
|
+
before: params.before,
|
|
808
|
+
includeArchived: params.includeArchived,
|
|
809
|
+
orderBy: normalizeOrderBy4(params.orderBy)
|
|
810
|
+
}
|
|
811
|
+
);
|
|
812
|
+
if (data.teams.nodes.length === 0) {
|
|
813
|
+
return "No teams found.";
|
|
814
|
+
}
|
|
815
|
+
return formatConnection(data.teams, formatTeamSummary);
|
|
816
|
+
}
|
|
817
|
+
async function getTeam(client, params) {
|
|
818
|
+
if (isUUID(params.query)) {
|
|
819
|
+
const data2 = await client.query(
|
|
820
|
+
`query($id: String!) {
|
|
821
|
+
team(id: $id) { ${TEAM_FIELDS} }
|
|
822
|
+
}`,
|
|
823
|
+
{ id: params.query }
|
|
824
|
+
);
|
|
825
|
+
return formatTeamDetail(data2.team);
|
|
826
|
+
}
|
|
827
|
+
const filter = {
|
|
828
|
+
or: [{ name: { containsIgnoreCase: params.query } }, { key: { eq: params.query } }]
|
|
829
|
+
};
|
|
830
|
+
const data = await client.query(
|
|
831
|
+
`query($filter: TeamFilter, $first: Int) {
|
|
832
|
+
teams(filter: $filter, first: $first) {
|
|
833
|
+
nodes { ${TEAM_FIELDS} }
|
|
834
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
835
|
+
}
|
|
836
|
+
}`,
|
|
837
|
+
{ filter, first: 5 }
|
|
838
|
+
);
|
|
839
|
+
if (data.teams.nodes.length === 0) {
|
|
840
|
+
throw new Error(`No team matching "${params.query}" found.`);
|
|
841
|
+
}
|
|
842
|
+
if (data.teams.nodes.length === 1) {
|
|
843
|
+
const first = data.teams.nodes[0];
|
|
844
|
+
if (!first) throw new Error(`No team matching "${params.query}" found.`);
|
|
845
|
+
return formatTeamDetail(first);
|
|
846
|
+
}
|
|
847
|
+
return formatConnection(data.teams, formatTeamSummary);
|
|
848
|
+
}
|
|
849
|
+
var USER_FIELDS = `id name displayName email active admin avatarUrl createdAt`;
|
|
850
|
+
function formatUserSummary(user) {
|
|
851
|
+
const status = user.active ? "active" : "deactivated";
|
|
852
|
+
const admin = user.admin ? " (admin)" : "";
|
|
853
|
+
return `${user.displayName} <${user.email}> [${status}${admin}]
|
|
854
|
+
ID: ${user.id}`;
|
|
855
|
+
}
|
|
856
|
+
function formatUserDetail(user) {
|
|
857
|
+
return [
|
|
858
|
+
`# ${user.displayName}`,
|
|
859
|
+
"",
|
|
860
|
+
`**Name:** ${user.name}`,
|
|
861
|
+
`**Email:** ${user.email}`,
|
|
862
|
+
`**Active:** ${user.active}`,
|
|
863
|
+
`**Admin:** ${user.admin}`,
|
|
864
|
+
`**ID:** ${user.id}`,
|
|
865
|
+
`**Created:** ${user.createdAt}`
|
|
866
|
+
].join("\n");
|
|
867
|
+
}
|
|
868
|
+
async function listUsers(client) {
|
|
869
|
+
const data = await client.query(
|
|
870
|
+
`query($first: Int) {
|
|
871
|
+
users(first: $first) {
|
|
872
|
+
nodes { ${USER_FIELDS} }
|
|
873
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
874
|
+
}
|
|
875
|
+
}`,
|
|
876
|
+
{ first: 50 }
|
|
877
|
+
);
|
|
878
|
+
if (data.users.nodes.length === 0) {
|
|
879
|
+
return "No users found.";
|
|
880
|
+
}
|
|
881
|
+
return formatConnection(data.users, formatUserSummary);
|
|
882
|
+
}
|
|
883
|
+
async function getUser(client, params) {
|
|
884
|
+
if (isUUID(params.query)) {
|
|
885
|
+
const data2 = await client.query(
|
|
886
|
+
`query($id: String!) {
|
|
887
|
+
user(id: $id) { ${USER_FIELDS} }
|
|
888
|
+
}`,
|
|
889
|
+
{ id: params.query }
|
|
890
|
+
);
|
|
891
|
+
return formatUserDetail(data2.user);
|
|
892
|
+
}
|
|
893
|
+
const filter = {
|
|
894
|
+
or: [
|
|
895
|
+
{ name: { containsIgnoreCase: params.query } },
|
|
896
|
+
{ email: { containsIgnoreCase: params.query } },
|
|
897
|
+
{ displayName: { containsIgnoreCase: params.query } }
|
|
898
|
+
]
|
|
899
|
+
};
|
|
900
|
+
const data = await client.query(
|
|
901
|
+
`query($filter: UserFilter) {
|
|
902
|
+
users(filter: $filter) {
|
|
903
|
+
nodes { ${USER_FIELDS} }
|
|
904
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
905
|
+
}
|
|
906
|
+
}`,
|
|
907
|
+
{ filter }
|
|
908
|
+
);
|
|
909
|
+
if (data.users.nodes.length === 0) {
|
|
910
|
+
throw new Error(`No user matching "${params.query}" found.`);
|
|
911
|
+
}
|
|
912
|
+
if (data.users.nodes.length === 1) {
|
|
913
|
+
const first = data.users.nodes[0];
|
|
914
|
+
if (!first) throw new Error(`No user matching "${params.query}" found.`);
|
|
915
|
+
return formatUserDetail(first);
|
|
916
|
+
}
|
|
917
|
+
return formatConnection(data.users, formatUserSummary);
|
|
918
|
+
}
|
|
919
|
+
function formatWorkflowState(state) {
|
|
920
|
+
return `[${state.type}] ${state.name} (${state.color})
|
|
921
|
+
ID: ${state.id}`;
|
|
922
|
+
}
|
|
923
|
+
function formatLabel(label) {
|
|
924
|
+
const parent = label.parent ? ` (parent: ${label.parent.name})` : "";
|
|
925
|
+
const desc = label.description ? ` - ${label.description}` : "";
|
|
926
|
+
return `${label.name}${parent}${desc}
|
|
927
|
+
ID: ${label.id} | Color: ${label.color}`;
|
|
928
|
+
}
|
|
929
|
+
async function listIssueStatuses(client, params) {
|
|
930
|
+
const teamId = await resolveTeamId(client, params.teamId);
|
|
931
|
+
const filter = { team: { id: { eq: teamId } } };
|
|
932
|
+
const data = await client.query(
|
|
933
|
+
`query($filter: WorkflowStateFilter) {
|
|
934
|
+
workflowStates(filter: $filter) {
|
|
935
|
+
nodes {
|
|
936
|
+
id name type color position description
|
|
937
|
+
team { id key name }
|
|
938
|
+
}
|
|
939
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
940
|
+
}
|
|
941
|
+
}`,
|
|
942
|
+
{ filter }
|
|
943
|
+
);
|
|
944
|
+
if (data.workflowStates.nodes.length === 0) {
|
|
945
|
+
return "No workflow states found for this team.";
|
|
946
|
+
}
|
|
947
|
+
return formatConnection(data.workflowStates, formatWorkflowState);
|
|
948
|
+
}
|
|
949
|
+
async function getIssueStatus(client, params) {
|
|
950
|
+
if (isUUID(params.query)) {
|
|
951
|
+
const data2 = await client.query(
|
|
952
|
+
`query($id: String!) {
|
|
953
|
+
workflowState(id: $id) {
|
|
954
|
+
id name type color position description
|
|
955
|
+
team { id key name }
|
|
956
|
+
}
|
|
957
|
+
}`,
|
|
958
|
+
{ id: params.query }
|
|
959
|
+
);
|
|
960
|
+
return formatWorkflowState(data2.workflowState);
|
|
961
|
+
}
|
|
962
|
+
const teamId = await resolveTeamId(client, params.teamId);
|
|
963
|
+
const filter = {
|
|
964
|
+
team: { id: { eq: teamId } },
|
|
965
|
+
name: { containsIgnoreCase: params.query }
|
|
966
|
+
};
|
|
967
|
+
const data = await client.query(
|
|
968
|
+
`query($filter: WorkflowStateFilter) {
|
|
969
|
+
workflowStates(filter: $filter) {
|
|
970
|
+
nodes {
|
|
971
|
+
id name type color position description
|
|
972
|
+
team { id key name }
|
|
973
|
+
}
|
|
974
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
975
|
+
}
|
|
976
|
+
}`,
|
|
977
|
+
{ filter }
|
|
978
|
+
);
|
|
979
|
+
if (data.workflowStates.nodes.length === 0) {
|
|
980
|
+
throw new Error(`No workflow state matching "${params.query}" found in this team.`);
|
|
981
|
+
}
|
|
982
|
+
return data.workflowStates.nodes.map(formatWorkflowState).join("\n\n");
|
|
983
|
+
}
|
|
984
|
+
async function listIssueLabels(client, params) {
|
|
985
|
+
const teamId = await resolveTeamId(client, params.teamId);
|
|
986
|
+
const filter = { team: { id: { eq: teamId } } };
|
|
987
|
+
const data = await client.query(
|
|
988
|
+
`query($filter: IssueLabelFilter) {
|
|
989
|
+
issueLabels(filter: $filter) {
|
|
990
|
+
nodes {
|
|
991
|
+
id name color description
|
|
992
|
+
parent { id name }
|
|
993
|
+
}
|
|
994
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
995
|
+
}
|
|
996
|
+
}`,
|
|
997
|
+
{ filter }
|
|
998
|
+
);
|
|
999
|
+
if (data.issueLabels.nodes.length === 0) {
|
|
1000
|
+
return "No labels found for this team.";
|
|
1001
|
+
}
|
|
1002
|
+
return formatConnection(data.issueLabels, formatLabel);
|
|
1003
|
+
}
|
|
1004
|
+
var TOOL_DEFINITIONS = [
|
|
1005
|
+
{
|
|
1006
|
+
name: "list_issues",
|
|
1007
|
+
description: "List issues in the user's Linear workspace. Returns issue identifiers (e.g., 'ENG-123') that can be passed directly to get_issue, update_issue, or delete_issue. Use 'query' for text search (pagination and ordering not available for text search), or structured filters with pagination. To filter by status name, first call list_issue_statuses to get the stateId. To filter by assignee name, first call list_users to get the userId, or use 'me' for the current user.",
|
|
1008
|
+
inputSchema: {
|
|
1009
|
+
type: "object",
|
|
1010
|
+
properties: {
|
|
1011
|
+
query: { type: "string", description: "Text search query to filter issues" },
|
|
1012
|
+
teamId: {
|
|
1013
|
+
type: "string",
|
|
1014
|
+
description: "Filter by team ID (UUID) or team key (e.g., 'ENG')"
|
|
1015
|
+
},
|
|
1016
|
+
stateId: {
|
|
1017
|
+
type: "string",
|
|
1018
|
+
description: "Filter by workflow state ID (UUID). Call list_issue_statuses to find the ID for a status name."
|
|
1019
|
+
},
|
|
1020
|
+
assigneeId: {
|
|
1021
|
+
type: "string",
|
|
1022
|
+
description: "Filter by assignee user ID. Use 'me' for current user."
|
|
1023
|
+
},
|
|
1024
|
+
parentId: {
|
|
1025
|
+
type: "string",
|
|
1026
|
+
description: "Filter by parent issue ID (for sub-issues)"
|
|
1027
|
+
},
|
|
1028
|
+
projectId: { type: "string", description: "Filter by project ID" },
|
|
1029
|
+
createdAt: {
|
|
1030
|
+
type: "string",
|
|
1031
|
+
description: "Filter to issues created at or after this date (ISO 8601)"
|
|
1032
|
+
},
|
|
1033
|
+
updatedAt: {
|
|
1034
|
+
type: "string",
|
|
1035
|
+
description: "Filter to issues updated at or after this date (ISO 8601)"
|
|
1036
|
+
},
|
|
1037
|
+
includeArchived: {
|
|
1038
|
+
type: "boolean",
|
|
1039
|
+
description: "Include archived issues in results"
|
|
1040
|
+
},
|
|
1041
|
+
limit: {
|
|
1042
|
+
type: "number",
|
|
1043
|
+
description: "Maximum number of results to return (default: 25, max: 100)"
|
|
1044
|
+
},
|
|
1045
|
+
after: {
|
|
1046
|
+
type: "string",
|
|
1047
|
+
description: "Cursor for forward pagination (from previous results)"
|
|
1048
|
+
},
|
|
1049
|
+
orderBy: {
|
|
1050
|
+
type: "string",
|
|
1051
|
+
enum: ["createdAt", "updatedAt"],
|
|
1052
|
+
description: "Order results by 'createdAt' or 'updatedAt'"
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
name: "get_issue",
|
|
1059
|
+
description: "Retrieve a Linear issue's full details including title, description, attachments, comments (with comment UUIDs), and metadata. Accepts a UUID, an identifier like 'ENG-123', or a text search query.",
|
|
1060
|
+
inputSchema: {
|
|
1061
|
+
type: "object",
|
|
1062
|
+
properties: {
|
|
1063
|
+
id: {
|
|
1064
|
+
type: "string",
|
|
1065
|
+
description: "Issue UUID, identifier (e.g., 'ENG-123'), or search text. Identifiers are case-insensitive."
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
required: ["id"]
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
name: "create_issue",
|
|
1073
|
+
description: "Create a new Linear issue or sub-issue. Before calling, use list_teams to resolve the team key and list_issue_statuses for valid stateId values.",
|
|
1074
|
+
inputSchema: {
|
|
1075
|
+
type: "object",
|
|
1076
|
+
properties: {
|
|
1077
|
+
title: { type: "string", description: "Issue title (required)" },
|
|
1078
|
+
teamId: {
|
|
1079
|
+
type: "string",
|
|
1080
|
+
description: "Team ID (UUID) or key (e.g., 'ENG'). Required."
|
|
1081
|
+
},
|
|
1082
|
+
description: {
|
|
1083
|
+
type: "string",
|
|
1084
|
+
description: "Issue description. Supports markdown"
|
|
1085
|
+
},
|
|
1086
|
+
priority: {
|
|
1087
|
+
type: "number",
|
|
1088
|
+
description: "Priority level: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low",
|
|
1089
|
+
enum: [0, 1, 2, 3, 4]
|
|
1090
|
+
},
|
|
1091
|
+
projectId: {
|
|
1092
|
+
type: "string",
|
|
1093
|
+
description: "Project ID to associate the issue with"
|
|
1094
|
+
},
|
|
1095
|
+
parentId: {
|
|
1096
|
+
type: "string",
|
|
1097
|
+
description: "Parent issue ID to create as sub-issue. teamId is still required."
|
|
1098
|
+
},
|
|
1099
|
+
stateId: {
|
|
1100
|
+
type: "string",
|
|
1101
|
+
description: "Workflow state ID (UUID) for initial status. Call list_issue_statuses to find valid IDs."
|
|
1102
|
+
},
|
|
1103
|
+
assigneeId: {
|
|
1104
|
+
type: "string",
|
|
1105
|
+
description: "User ID to assign. Use 'me' for self-assignment."
|
|
1106
|
+
},
|
|
1107
|
+
labelIds: {
|
|
1108
|
+
type: "array",
|
|
1109
|
+
items: { type: "string" },
|
|
1110
|
+
description: "Array of label IDs to apply"
|
|
1111
|
+
},
|
|
1112
|
+
dueDate: {
|
|
1113
|
+
type: "string",
|
|
1114
|
+
description: "Due date in ISO 8601 format (YYYY-MM-DD)"
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
required: ["title", "teamId"]
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
name: "update_issue",
|
|
1122
|
+
description: "Update an existing Linear issue. Accepts UUID or identifier (e.g., 'ENG-123'). Only provided fields are changed; omitted fields are left unchanged. Use list_issue_statuses to find stateId values.",
|
|
1123
|
+
inputSchema: {
|
|
1124
|
+
type: "object",
|
|
1125
|
+
properties: {
|
|
1126
|
+
id: {
|
|
1127
|
+
type: "string",
|
|
1128
|
+
description: "Issue UUID or identifier (e.g., 'ENG-123')"
|
|
1129
|
+
},
|
|
1130
|
+
title: { type: "string", description: "New issue title" },
|
|
1131
|
+
description: {
|
|
1132
|
+
type: "string",
|
|
1133
|
+
description: "New issue description (markdown)"
|
|
1134
|
+
},
|
|
1135
|
+
priority: {
|
|
1136
|
+
type: "number",
|
|
1137
|
+
description: "Priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low",
|
|
1138
|
+
enum: [0, 1, 2, 3, 4]
|
|
1139
|
+
},
|
|
1140
|
+
projectId: {
|
|
1141
|
+
type: "string",
|
|
1142
|
+
description: "Project ID to move issue to"
|
|
1143
|
+
},
|
|
1144
|
+
parentId: {
|
|
1145
|
+
type: "string",
|
|
1146
|
+
description: "Parent issue ID (convert to sub-issue)"
|
|
1147
|
+
},
|
|
1148
|
+
stateId: {
|
|
1149
|
+
type: "string",
|
|
1150
|
+
description: "Workflow state ID (UUID). Call list_issue_statuses to find valid IDs."
|
|
1151
|
+
},
|
|
1152
|
+
assigneeId: {
|
|
1153
|
+
type: "string",
|
|
1154
|
+
description: "User ID to assign (use 'me' for self, null to unassign)"
|
|
1155
|
+
},
|
|
1156
|
+
labelIds: {
|
|
1157
|
+
type: "array",
|
|
1158
|
+
items: { type: "string" },
|
|
1159
|
+
description: "Replacement label IDs (overwrites existing)"
|
|
1160
|
+
},
|
|
1161
|
+
dueDate: {
|
|
1162
|
+
type: "string",
|
|
1163
|
+
description: "Due date (ISO 8601 YYYY-MM-DD)"
|
|
1164
|
+
},
|
|
1165
|
+
estimate: {
|
|
1166
|
+
type: "number",
|
|
1167
|
+
description: "Complexity/effort estimate points"
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
required: ["id"]
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
name: "delete_issue",
|
|
1175
|
+
description: "DESTRUCTIVE: Permanently delete a Linear issue by UUID or identifier (e.g., 'ENG-123'). This cannot be undone. Always confirm with the user before deleting.",
|
|
1176
|
+
inputSchema: {
|
|
1177
|
+
type: "object",
|
|
1178
|
+
properties: {
|
|
1179
|
+
id: {
|
|
1180
|
+
type: "string",
|
|
1181
|
+
description: "Issue UUID or identifier (e.g., 'ENG-123')"
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
required: ["id"]
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
name: "list_my_issues",
|
|
1189
|
+
description: "List issues assigned to you (the authenticated user), with cursor-based pagination and ordering. Use this for browsing your issues; use list_issues for filtered searches across the workspace.",
|
|
1190
|
+
inputSchema: {
|
|
1191
|
+
type: "object",
|
|
1192
|
+
properties: {
|
|
1193
|
+
limit: { type: "number", description: "Maximum results to return" },
|
|
1194
|
+
before: {
|
|
1195
|
+
type: "string",
|
|
1196
|
+
description: "Cursor for backward pagination"
|
|
1197
|
+
},
|
|
1198
|
+
after: {
|
|
1199
|
+
type: "string",
|
|
1200
|
+
description: "Cursor for forward pagination"
|
|
1201
|
+
},
|
|
1202
|
+
orderBy: {
|
|
1203
|
+
type: "string",
|
|
1204
|
+
enum: ["createdAt", "updatedAt"],
|
|
1205
|
+
description: "Order results by 'createdAt' or 'updatedAt'"
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
name: "get_issue_git_branch_name",
|
|
1212
|
+
description: "Get the git branch name that Linear auto-generates for an issue, used to link commits and PRs back to the issue. Accepts UUID or identifier (e.g., 'ENG-123').",
|
|
1213
|
+
inputSchema: {
|
|
1214
|
+
type: "object",
|
|
1215
|
+
properties: {
|
|
1216
|
+
id: {
|
|
1217
|
+
type: "string",
|
|
1218
|
+
description: "Issue UUID or identifier (e.g., 'ENG-123')"
|
|
1219
|
+
}
|
|
1220
|
+
},
|
|
1221
|
+
required: ["id"]
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
name: "list_issue_statuses",
|
|
1226
|
+
description: "List available workflow statuses for a Linear team. Returns status IDs needed for the stateId parameter in create_issue, update_issue, and list_issues filters. Accepts team UUID or key (e.g., 'ENG').",
|
|
1227
|
+
inputSchema: {
|
|
1228
|
+
type: "object",
|
|
1229
|
+
properties: {
|
|
1230
|
+
teamId: {
|
|
1231
|
+
type: "string",
|
|
1232
|
+
description: "Team ID (UUID) or key (e.g., 'ENG')"
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
required: ["teamId"]
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
name: "get_issue_status",
|
|
1240
|
+
description: "Retrieve details of a specific workflow state by name or ID within a team.",
|
|
1241
|
+
inputSchema: {
|
|
1242
|
+
type: "object",
|
|
1243
|
+
properties: {
|
|
1244
|
+
query: {
|
|
1245
|
+
type: "string",
|
|
1246
|
+
description: "Status name or ID to look up"
|
|
1247
|
+
},
|
|
1248
|
+
teamId: {
|
|
1249
|
+
type: "string",
|
|
1250
|
+
description: "Team ID the status belongs to"
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1253
|
+
required: ["query", "teamId"]
|
|
1254
|
+
}
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
name: "list_issue_labels",
|
|
1258
|
+
description: "List available issue labels for a Linear team. Returns label IDs needed for the labelIds parameter in create_issue and update_issue. Accepts team UUID or key (e.g., 'ENG').",
|
|
1259
|
+
inputSchema: {
|
|
1260
|
+
type: "object",
|
|
1261
|
+
properties: {
|
|
1262
|
+
teamId: {
|
|
1263
|
+
type: "string",
|
|
1264
|
+
description: "Team ID (UUID) or key (e.g., 'ENG')"
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
1267
|
+
required: ["teamId"]
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
name: "list_projects",
|
|
1272
|
+
description: "List projects in the user's Linear workspace. Returns project names and UUIDs. To update or delete a project, use the UUID from the output or call get_project with the project name.",
|
|
1273
|
+
inputSchema: {
|
|
1274
|
+
type: "object",
|
|
1275
|
+
properties: {
|
|
1276
|
+
limit: { type: "number", description: "Maximum results to return" },
|
|
1277
|
+
before: {
|
|
1278
|
+
type: "string",
|
|
1279
|
+
description: "Cursor for backward pagination"
|
|
1280
|
+
},
|
|
1281
|
+
after: {
|
|
1282
|
+
type: "string",
|
|
1283
|
+
description: "Cursor for forward pagination"
|
|
1284
|
+
},
|
|
1285
|
+
orderBy: {
|
|
1286
|
+
type: "string",
|
|
1287
|
+
enum: ["createdAt", "updatedAt"],
|
|
1288
|
+
description: "Order results by 'createdAt' or 'updatedAt'"
|
|
1289
|
+
},
|
|
1290
|
+
includeArchived: {
|
|
1291
|
+
type: "boolean",
|
|
1292
|
+
description: "Include archived projects"
|
|
1293
|
+
},
|
|
1294
|
+
teamId: {
|
|
1295
|
+
type: "string",
|
|
1296
|
+
description: "Filter by team ID (UUID) or team key (e.g., 'ENG')"
|
|
1297
|
+
},
|
|
1298
|
+
createdAt: {
|
|
1299
|
+
type: "string",
|
|
1300
|
+
description: "Filter by creation date"
|
|
1301
|
+
},
|
|
1302
|
+
updatedAt: {
|
|
1303
|
+
type: "string",
|
|
1304
|
+
description: "Filter by last update date"
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
},
|
|
1309
|
+
{
|
|
1310
|
+
name: "get_project",
|
|
1311
|
+
description: "Retrieve details of a specific Linear project. Accepts a project name (fuzzy match) or UUID. Returns full details including UUID, which is needed for update_project and delete_project.",
|
|
1312
|
+
inputSchema: {
|
|
1313
|
+
type: "object",
|
|
1314
|
+
properties: {
|
|
1315
|
+
query: {
|
|
1316
|
+
type: "string",
|
|
1317
|
+
description: "Project name (fuzzy match) or UUID"
|
|
1318
|
+
}
|
|
1319
|
+
},
|
|
1320
|
+
required: ["query"]
|
|
1321
|
+
}
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
name: "create_project",
|
|
1325
|
+
description: "Create a new project in Linear.",
|
|
1326
|
+
inputSchema: {
|
|
1327
|
+
type: "object",
|
|
1328
|
+
properties: {
|
|
1329
|
+
name: { type: "string", description: "Project name" },
|
|
1330
|
+
teamId: {
|
|
1331
|
+
type: "string",
|
|
1332
|
+
description: "Team ID (UUID) or key (e.g., 'ENG')"
|
|
1333
|
+
},
|
|
1334
|
+
summary: {
|
|
1335
|
+
type: "string",
|
|
1336
|
+
description: "Short project summary (max 255 characters)"
|
|
1337
|
+
},
|
|
1338
|
+
description: {
|
|
1339
|
+
type: "string",
|
|
1340
|
+
description: "Detailed project description (markdown)"
|
|
1341
|
+
},
|
|
1342
|
+
startDate: {
|
|
1343
|
+
type: "string",
|
|
1344
|
+
description: "Project start date (ISO 8601 YYYY-MM-DD)"
|
|
1345
|
+
},
|
|
1346
|
+
targetDate: {
|
|
1347
|
+
type: "string",
|
|
1348
|
+
description: "Project target completion date (ISO 8601 YYYY-MM-DD)"
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
required: ["name", "teamId"]
|
|
1352
|
+
}
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
name: "update_project",
|
|
1356
|
+
description: "Update an existing Linear project by UUID. To find a project UUID, call get_project with the project name. Only provided fields are changed; omitted fields are left unchanged.",
|
|
1357
|
+
inputSchema: {
|
|
1358
|
+
type: "object",
|
|
1359
|
+
properties: {
|
|
1360
|
+
id: {
|
|
1361
|
+
type: "string",
|
|
1362
|
+
description: "Project UUID. Call get_project with the project name to find this."
|
|
1363
|
+
},
|
|
1364
|
+
name: { type: "string", description: "New project name" },
|
|
1365
|
+
summary: {
|
|
1366
|
+
type: "string",
|
|
1367
|
+
description: "Short summary (max 255 characters)"
|
|
1368
|
+
},
|
|
1369
|
+
description: {
|
|
1370
|
+
type: "string",
|
|
1371
|
+
description: "Detailed description (markdown)"
|
|
1372
|
+
},
|
|
1373
|
+
startDate: {
|
|
1374
|
+
type: "string",
|
|
1375
|
+
description: "Start date (ISO 8601 YYYY-MM-DD)"
|
|
1376
|
+
},
|
|
1377
|
+
targetDate: {
|
|
1378
|
+
type: "string",
|
|
1379
|
+
description: "Target date (ISO 8601 YYYY-MM-DD)"
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
required: ["id"]
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
name: "delete_project",
|
|
1387
|
+
description: "DESTRUCTIVE: Permanently delete a Linear project by UUID. This cannot be undone. Call get_project with the project name to find the UUID. Always confirm with the user before deleting.",
|
|
1388
|
+
inputSchema: {
|
|
1389
|
+
type: "object",
|
|
1390
|
+
properties: {
|
|
1391
|
+
id: {
|
|
1392
|
+
type: "string",
|
|
1393
|
+
description: "Project UUID. Call get_project with the project name to find this."
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1396
|
+
required: ["id"]
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
name: "list_teams",
|
|
1401
|
+
description: "List teams in the user's Linear workspace. Returns team names, keys (e.g., 'ENG'), and UUIDs. Team keys can be used as teamId in other tools.",
|
|
1402
|
+
inputSchema: {
|
|
1403
|
+
type: "object",
|
|
1404
|
+
properties: {
|
|
1405
|
+
limit: { type: "number", description: "Maximum results to return" },
|
|
1406
|
+
before: {
|
|
1407
|
+
type: "string",
|
|
1408
|
+
description: "Cursor for backward pagination"
|
|
1409
|
+
},
|
|
1410
|
+
after: {
|
|
1411
|
+
type: "string",
|
|
1412
|
+
description: "Cursor for forward pagination"
|
|
1413
|
+
},
|
|
1414
|
+
orderBy: {
|
|
1415
|
+
type: "string",
|
|
1416
|
+
enum: ["createdAt", "updatedAt"],
|
|
1417
|
+
description: "Order results by 'createdAt' or 'updatedAt'"
|
|
1418
|
+
},
|
|
1419
|
+
query: {
|
|
1420
|
+
type: "string",
|
|
1421
|
+
description: "Filter by team name or key"
|
|
1422
|
+
},
|
|
1423
|
+
includeArchived: {
|
|
1424
|
+
type: "boolean",
|
|
1425
|
+
description: "Include archived teams"
|
|
1426
|
+
},
|
|
1427
|
+
createdAt: {
|
|
1428
|
+
type: "string",
|
|
1429
|
+
description: "Filter by creation date"
|
|
1430
|
+
},
|
|
1431
|
+
updatedAt: {
|
|
1432
|
+
type: "string",
|
|
1433
|
+
description: "Filter by update date"
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
name: "get_team",
|
|
1440
|
+
description: "Retrieve details of a specific Linear team by name, key, or ID.",
|
|
1441
|
+
inputSchema: {
|
|
1442
|
+
type: "object",
|
|
1443
|
+
properties: {
|
|
1444
|
+
query: {
|
|
1445
|
+
type: "string",
|
|
1446
|
+
description: "Team name, key, or ID to look up"
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
required: ["query"]
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
name: "list_users",
|
|
1454
|
+
description: "List users in the Linear workspace with their UUIDs, names, emails, and active status. Returns up to 50 users. User UUIDs can be used as assigneeId in other tools.",
|
|
1455
|
+
inputSchema: {
|
|
1456
|
+
type: "object",
|
|
1457
|
+
properties: {}
|
|
1458
|
+
}
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
name: "get_user",
|
|
1462
|
+
description: "Retrieve details of a specific Linear user by name, email, or ID.",
|
|
1463
|
+
inputSchema: {
|
|
1464
|
+
type: "object",
|
|
1465
|
+
properties: {
|
|
1466
|
+
query: {
|
|
1467
|
+
type: "string",
|
|
1468
|
+
description: "User name, email, or ID to look up"
|
|
1469
|
+
}
|
|
1470
|
+
},
|
|
1471
|
+
required: ["query"]
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
name: "list_comments",
|
|
1476
|
+
description: "Retrieve comments on a Linear issue. Returns comment text, authors, and comment UUIDs. Accepts an issue UUID or identifier (e.g., 'ENG-123'). To delete a specific comment, use the comment UUID from the output with delete_comment.",
|
|
1477
|
+
inputSchema: {
|
|
1478
|
+
type: "object",
|
|
1479
|
+
properties: {
|
|
1480
|
+
issueId: {
|
|
1481
|
+
type: "string",
|
|
1482
|
+
description: "Issue UUID or identifier (e.g., 'ENG-123')"
|
|
1483
|
+
}
|
|
1484
|
+
},
|
|
1485
|
+
required: ["issueId"]
|
|
1486
|
+
}
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
name: "create_comment",
|
|
1490
|
+
description: "Create a new comment on a Linear issue. Accepts issue UUID or identifier (e.g., 'ENG-123').",
|
|
1491
|
+
inputSchema: {
|
|
1492
|
+
type: "object",
|
|
1493
|
+
properties: {
|
|
1494
|
+
issueId: {
|
|
1495
|
+
type: "string",
|
|
1496
|
+
description: "Issue UUID or identifier (e.g., 'ENG-123')"
|
|
1497
|
+
},
|
|
1498
|
+
body: {
|
|
1499
|
+
type: "string",
|
|
1500
|
+
description: "Comment body text. Supports markdown formatting"
|
|
1501
|
+
}
|
|
1502
|
+
},
|
|
1503
|
+
required: ["issueId", "body"]
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
name: "delete_comment",
|
|
1508
|
+
description: "DESTRUCTIVE: Permanently delete a Linear issue comment by UUID. This cannot be undone. Comment UUIDs are returned by list_comments and get_issue. Always confirm with the user before deleting.",
|
|
1509
|
+
inputSchema: {
|
|
1510
|
+
type: "object",
|
|
1511
|
+
properties: {
|
|
1512
|
+
id: {
|
|
1513
|
+
type: "string",
|
|
1514
|
+
description: "Comment UUID. Find this via list_comments or get_issue."
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
required: ["id"]
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
{
|
|
1521
|
+
name: "get_document",
|
|
1522
|
+
description: "Retrieve a Linear document by ID or slug.",
|
|
1523
|
+
inputSchema: {
|
|
1524
|
+
type: "object",
|
|
1525
|
+
properties: {
|
|
1526
|
+
id: {
|
|
1527
|
+
type: "string",
|
|
1528
|
+
description: "Document ID (UUID) or slug"
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
1531
|
+
required: ["id"]
|
|
1532
|
+
}
|
|
1533
|
+
},
|
|
1534
|
+
{
|
|
1535
|
+
name: "list_documents",
|
|
1536
|
+
description: "List documents in the user's Linear workspace. Supports filtering by project, creator, title search, and date ranges.",
|
|
1537
|
+
inputSchema: {
|
|
1538
|
+
type: "object",
|
|
1539
|
+
properties: {
|
|
1540
|
+
limit: { type: "number", description: "Maximum results to return" },
|
|
1541
|
+
before: {
|
|
1542
|
+
type: "string",
|
|
1543
|
+
description: "Cursor for backward pagination"
|
|
1544
|
+
},
|
|
1545
|
+
after: {
|
|
1546
|
+
type: "string",
|
|
1547
|
+
description: "Cursor for forward pagination"
|
|
1548
|
+
},
|
|
1549
|
+
orderBy: {
|
|
1550
|
+
type: "string",
|
|
1551
|
+
enum: ["createdAt", "updatedAt"],
|
|
1552
|
+
description: "Order results by 'createdAt' or 'updatedAt'"
|
|
1553
|
+
},
|
|
1554
|
+
query: { type: "string", description: "Text search query" },
|
|
1555
|
+
projectId: { type: "string", description: "Filter by project ID" },
|
|
1556
|
+
creatorId: {
|
|
1557
|
+
type: "string",
|
|
1558
|
+
description: "Filter by creator user ID"
|
|
1559
|
+
},
|
|
1560
|
+
createdAt: {
|
|
1561
|
+
type: "string",
|
|
1562
|
+
description: "Filter by creation date"
|
|
1563
|
+
},
|
|
1564
|
+
updatedAt: {
|
|
1565
|
+
type: "string",
|
|
1566
|
+
description: "Filter by update date"
|
|
1567
|
+
},
|
|
1568
|
+
includeArchived: {
|
|
1569
|
+
type: "boolean",
|
|
1570
|
+
description: "Include archived documents"
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
name: "search_documentation",
|
|
1577
|
+
description: "Search Linear's own documentation to learn about Linear features and usage. This is NOT for searching user workspace data -- it searches Linear's help docs.",
|
|
1578
|
+
inputSchema: {
|
|
1579
|
+
type: "object",
|
|
1580
|
+
properties: {
|
|
1581
|
+
query: {
|
|
1582
|
+
type: "string",
|
|
1583
|
+
description: "Search query for Linear documentation"
|
|
1584
|
+
},
|
|
1585
|
+
page: {
|
|
1586
|
+
type: "number",
|
|
1587
|
+
description: "Page number for paginated results"
|
|
1588
|
+
}
|
|
1589
|
+
},
|
|
1590
|
+
required: ["query"]
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
];
|
|
1594
|
+
function getClient() {
|
|
1595
|
+
const token = process.env.LINEAR_ACCESS_TOKEN;
|
|
1596
|
+
if (!token) {
|
|
1597
|
+
throw new Error(
|
|
1598
|
+
"LINEAR_ACCESS_TOKEN environment variable is required. Set it to a Linear API key or OAuth access token."
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
return new LinearGraphQLClient(token);
|
|
1602
|
+
}
|
|
1603
|
+
async function handleToolCall(name, args) {
|
|
1604
|
+
const client = getClient();
|
|
1605
|
+
const a = args;
|
|
1606
|
+
switch (name) {
|
|
1607
|
+
case "list_issues":
|
|
1608
|
+
return listIssues(client, a);
|
|
1609
|
+
case "get_issue":
|
|
1610
|
+
return getIssue(client, a);
|
|
1611
|
+
case "create_issue":
|
|
1612
|
+
return createIssue(client, a);
|
|
1613
|
+
case "update_issue":
|
|
1614
|
+
return updateIssue(client, a);
|
|
1615
|
+
case "delete_issue":
|
|
1616
|
+
return deleteIssue(client, a);
|
|
1617
|
+
case "list_my_issues":
|
|
1618
|
+
return listMyIssues(client, a);
|
|
1619
|
+
case "get_issue_git_branch_name":
|
|
1620
|
+
return getIssueGitBranchName(client, a);
|
|
1621
|
+
case "list_issue_statuses":
|
|
1622
|
+
return listIssueStatuses(client, a);
|
|
1623
|
+
case "get_issue_status":
|
|
1624
|
+
return getIssueStatus(client, a);
|
|
1625
|
+
case "list_issue_labels":
|
|
1626
|
+
return listIssueLabels(client, a);
|
|
1627
|
+
case "list_projects":
|
|
1628
|
+
return listProjects(client, a);
|
|
1629
|
+
case "get_project":
|
|
1630
|
+
return getProject(client, a);
|
|
1631
|
+
case "create_project":
|
|
1632
|
+
return createProject(client, a);
|
|
1633
|
+
case "update_project":
|
|
1634
|
+
return updateProject(client, a);
|
|
1635
|
+
case "delete_project":
|
|
1636
|
+
return deleteProject(client, a);
|
|
1637
|
+
case "list_teams":
|
|
1638
|
+
return listTeams(client, a);
|
|
1639
|
+
case "get_team":
|
|
1640
|
+
return getTeam(client, a);
|
|
1641
|
+
case "list_users":
|
|
1642
|
+
return listUsers(client);
|
|
1643
|
+
case "get_user":
|
|
1644
|
+
return getUser(client, a);
|
|
1645
|
+
case "list_comments":
|
|
1646
|
+
return listComments(client, a);
|
|
1647
|
+
case "create_comment":
|
|
1648
|
+
return createComment(client, a);
|
|
1649
|
+
case "delete_comment":
|
|
1650
|
+
return deleteComment(client, a);
|
|
1651
|
+
case "get_document":
|
|
1652
|
+
return getDocument(client, a);
|
|
1653
|
+
case "list_documents":
|
|
1654
|
+
return listDocuments(client, a);
|
|
1655
|
+
case "search_documentation":
|
|
1656
|
+
return searchDocumentation(client, a);
|
|
1657
|
+
default:
|
|
1658
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
async function main() {
|
|
1662
|
+
const server = new Server(
|
|
1663
|
+
{ name: "@shipyard/linear-mcp", version: "0.1.0" },
|
|
1664
|
+
{ capabilities: { tools: {} } }
|
|
1665
|
+
);
|
|
1666
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1667
|
+
tools: [...TOOL_DEFINITIONS]
|
|
1668
|
+
}));
|
|
1669
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1670
|
+
const { name, arguments: args } = request.params;
|
|
1671
|
+
try {
|
|
1672
|
+
const result = await handleToolCall(name, args ?? {});
|
|
1673
|
+
return { content: [{ type: "text", text: result }] };
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1676
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
const transport = new StdioServerTransport();
|
|
1680
|
+
await server.connect(transport);
|
|
1681
|
+
}
|
|
1682
|
+
main();
|
|
1683
|
+
//# sourceMappingURL=plugin-mcp-linear.js.map
|