@universal-mcp-toolkit/server-linear 0.1.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/index.js ADDED
@@ -0,0 +1,985 @@
1
+ // src/index.ts
2
+ import { pathToFileURL } from "url";
3
+ import {
4
+ ConfigurationError,
5
+ ExternalServiceError,
6
+ ValidationError,
7
+ createServerCard,
8
+ defineTool,
9
+ loadEnv,
10
+ normalizeError,
11
+ parseRuntimeOptions,
12
+ runToolkitServer,
13
+ ToolkitServer
14
+ } from "@universal-mcp-toolkit/core";
15
+ import { z } from "zod";
16
+ var DEFAULT_LINEAR_API_URL = "https://api.linear.app/graphql";
17
+ var TEAM_RESOURCE_URI = "linear://team/default";
18
+ var TOOL_NAMES = ["search_issues", "get_issue", "create_issue"];
19
+ var RESOURCE_NAMES = ["team"];
20
+ var PROMPT_NAMES = ["sprint-triage"];
21
+ function preprocessOptionalTrimmedString(value) {
22
+ if (typeof value !== "string") {
23
+ return value;
24
+ }
25
+ const trimmed = value.trim();
26
+ return trimmed.length === 0 ? void 0 : trimmed;
27
+ }
28
+ var requiredTrimmedStringSchema = z.string().trim().min(1);
29
+ var optionalTrimmedStringSchema = z.preprocess(preprocessOptionalTrimmedString, z.string().min(1).optional());
30
+ var optionalTeamKeySchema = z.preprocess((value) => {
31
+ if (typeof value !== "string") {
32
+ return value;
33
+ }
34
+ const normalized = value.trim().toUpperCase();
35
+ return normalized.length === 0 ? void 0 : normalized;
36
+ }, z.string().min(1).optional());
37
+ var optionalUrlSchema = z.preprocess(preprocessOptionalTrimmedString, z.string().url().optional());
38
+ var dueDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD.");
39
+ var linearEnvShape = {
40
+ LINEAR_API_KEY: requiredTrimmedStringSchema,
41
+ LINEAR_DEFAULT_TEAM_ID: optionalTrimmedStringSchema,
42
+ LINEAR_DEFAULT_TEAM_KEY: optionalTeamKeySchema,
43
+ LINEAR_WORKSPACE_NAME: optionalTrimmedStringSchema,
44
+ LINEAR_API_URL: optionalUrlSchema
45
+ };
46
+ var teamShape = {
47
+ id: z.string(),
48
+ key: z.string(),
49
+ name: z.string()
50
+ };
51
+ var teamSchema = z.object(teamShape);
52
+ var userShape = {
53
+ id: z.string(),
54
+ name: z.string()
55
+ };
56
+ var userSchema = z.object(userShape);
57
+ var issueStateShape = {
58
+ name: z.string().nullable(),
59
+ type: z.string().nullable()
60
+ };
61
+ var issueStateSchema = z.object(issueStateShape);
62
+ var issueCycleShape = {
63
+ id: z.string(),
64
+ number: z.number().int().nullable(),
65
+ name: z.string().nullable(),
66
+ startsAt: z.string().nullable(),
67
+ endsAt: z.string().nullable()
68
+ };
69
+ var issueCycleSchema = z.object(issueCycleShape);
70
+ var issueSummaryShape = {
71
+ id: z.string(),
72
+ identifier: z.string(),
73
+ title: z.string(),
74
+ url: z.string().nullable(),
75
+ priority: z.number().int().nullable(),
76
+ state: issueStateSchema,
77
+ team: teamSchema.nullable(),
78
+ assignee: userSchema.nullable(),
79
+ createdAt: z.string().nullable(),
80
+ updatedAt: z.string().nullable()
81
+ };
82
+ var issueSummarySchema = z.object(issueSummaryShape);
83
+ var issueDetailShape = {
84
+ ...issueSummaryShape,
85
+ description: z.string().nullable(),
86
+ branchName: z.string().nullable(),
87
+ cycle: issueCycleSchema.nullable()
88
+ };
89
+ var issueDetailSchema = z.object(issueDetailShape);
90
+ var searchIssuesInputShape = {
91
+ query: requiredTrimmedStringSchema,
92
+ limit: z.number().int().min(1).max(50).default(10),
93
+ teamId: optionalTrimmedStringSchema,
94
+ teamKey: optionalTeamKeySchema,
95
+ stateName: optionalTrimmedStringSchema
96
+ };
97
+ var searchIssuesOutputShape = {
98
+ query: z.string(),
99
+ stateName: z.string().nullable(),
100
+ team: teamSchema.nullable(),
101
+ warning: z.string().nullable(),
102
+ total: z.number().int().nonnegative(),
103
+ issues: z.array(issueSummarySchema)
104
+ };
105
+ var getIssueInputShape = {
106
+ idOrIdentifier: requiredTrimmedStringSchema
107
+ };
108
+ var getIssueOutputShape = {
109
+ issue: issueDetailSchema
110
+ };
111
+ var createIssueInputShape = {
112
+ title: requiredTrimmedStringSchema,
113
+ description: optionalTrimmedStringSchema,
114
+ teamId: optionalTrimmedStringSchema,
115
+ teamKey: optionalTeamKeySchema,
116
+ priority: z.number().int().min(0).max(4).optional(),
117
+ stateId: optionalTrimmedStringSchema,
118
+ assigneeId: optionalTrimmedStringSchema,
119
+ labelIds: z.array(requiredTrimmedStringSchema).max(25).optional(),
120
+ dueDate: dueDateSchema.optional(),
121
+ projectId: optionalTrimmedStringSchema,
122
+ cycleId: optionalTrimmedStringSchema
123
+ };
124
+ var createIssueOutputShape = {
125
+ created: z.boolean(),
126
+ issue: issueDetailSchema
127
+ };
128
+ var sprintTriagePromptArgsShape = {
129
+ teamId: optionalTrimmedStringSchema,
130
+ teamKey: optionalTeamKeySchema,
131
+ focus: optionalTrimmedStringSchema,
132
+ objective: optionalTrimmedStringSchema,
133
+ issueLimit: z.number().int().min(1).max(25).default(10),
134
+ includeBacklog: z.boolean().default(true)
135
+ };
136
+ var graphQlIssueStateSchema = z.object({
137
+ name: z.string(),
138
+ type: z.string().nullable().optional()
139
+ });
140
+ var graphQlIssueCycleSchema = z.object({
141
+ id: z.string(),
142
+ number: z.number().int().nullable().optional(),
143
+ name: z.string().nullable().optional(),
144
+ startsAt: z.string().nullable().optional(),
145
+ endsAt: z.string().nullable().optional()
146
+ });
147
+ var graphQlIssueSchema = z.object({
148
+ id: z.string(),
149
+ identifier: z.string(),
150
+ title: z.string(),
151
+ description: z.string().nullable().optional(),
152
+ url: z.string().nullable().optional(),
153
+ branchName: z.string().nullable().optional(),
154
+ priority: z.number().int().nullable().optional(),
155
+ createdAt: z.string().nullable().optional(),
156
+ updatedAt: z.string().nullable().optional(),
157
+ state: graphQlIssueStateSchema.nullable().optional(),
158
+ team: teamSchema.nullable().optional(),
159
+ assignee: userSchema.nullable().optional(),
160
+ cycle: graphQlIssueCycleSchema.nullable().optional()
161
+ });
162
+ var graphQlErrorSchema = z.object({
163
+ message: z.string(),
164
+ path: z.array(z.union([z.string(), z.number()])).optional(),
165
+ extensions: z.record(z.string(), z.unknown()).optional()
166
+ });
167
+ function createGraphQlEnvelopeSchema(dataSchema) {
168
+ return z.object({
169
+ data: dataSchema.optional(),
170
+ errors: z.array(graphQlErrorSchema).optional()
171
+ });
172
+ }
173
+ function normalizeTeamKey(value) {
174
+ if (value === null || value === void 0) {
175
+ return void 0;
176
+ }
177
+ const normalized = value.trim().toUpperCase();
178
+ return normalized.length === 0 ? void 0 : normalized;
179
+ }
180
+ function withOptionalProperty(key, value) {
181
+ return value === void 0 ? {} : { [key]: value };
182
+ }
183
+ function parseIssueIdentifier(value) {
184
+ const match = /^([A-Za-z][A-Za-z0-9]+)-(\d+)$/.exec(value.trim());
185
+ if (!match) {
186
+ return null;
187
+ }
188
+ const [, rawTeamKey, rawIssueNumber] = match;
189
+ if (rawTeamKey === void 0 || rawIssueNumber === void 0) {
190
+ return null;
191
+ }
192
+ const issueNumber = Number.parseInt(rawIssueNumber, 10);
193
+ if (!Number.isSafeInteger(issueNumber)) {
194
+ return null;
195
+ }
196
+ return {
197
+ teamKey: rawTeamKey.toUpperCase(),
198
+ number: issueNumber
199
+ };
200
+ }
201
+ function readUnknownRecord(value) {
202
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
203
+ return void 0;
204
+ }
205
+ return value;
206
+ }
207
+ function readStatusCode(value) {
208
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
209
+ return value;
210
+ }
211
+ if (typeof value === "string" && /^\d+$/.test(value)) {
212
+ const parsed = Number.parseInt(value, 10);
213
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
214
+ }
215
+ return void 0;
216
+ }
217
+ function extractGraphQlErrorStatus(error) {
218
+ const extensions = error.extensions;
219
+ if (!extensions) {
220
+ return void 0;
221
+ }
222
+ const directStatus = readStatusCode(extensions.statusCode) ?? readStatusCode(extensions.status);
223
+ if (directStatus !== void 0) {
224
+ return directStatus;
225
+ }
226
+ const httpExtensions = readUnknownRecord(extensions.http);
227
+ return httpExtensions ? readStatusCode(httpExtensions.status) : void 0;
228
+ }
229
+ function buildGraphQlError(operationName, errors, fallbackStatusCode = 502) {
230
+ const statusCode = errors.map((error) => extractGraphQlErrorStatus(error)).find((value) => value !== void 0) ?? fallbackStatusCode;
231
+ return new ExternalServiceError(`Linear ${operationName} failed: ${errors.map((error) => error.message).join("; ")}`, {
232
+ statusCode,
233
+ details: errors
234
+ });
235
+ }
236
+ function normalizeIssue(issue) {
237
+ return {
238
+ id: issue.id,
239
+ identifier: issue.identifier,
240
+ title: issue.title,
241
+ description: issue.description ?? null,
242
+ url: issue.url ?? null,
243
+ branchName: issue.branchName ?? null,
244
+ priority: issue.priority ?? null,
245
+ state: {
246
+ name: issue.state?.name ?? null,
247
+ type: issue.state?.type ?? null
248
+ },
249
+ team: issue.team ?? null,
250
+ assignee: issue.assignee ?? null,
251
+ cycle: issue.cycle ? {
252
+ id: issue.cycle.id,
253
+ number: issue.cycle.number ?? null,
254
+ name: issue.cycle.name ?? null,
255
+ startsAt: issue.cycle.startsAt ?? null,
256
+ endsAt: issue.cycle.endsAt ?? null
257
+ } : null,
258
+ createdAt: issue.createdAt ?? null,
259
+ updatedAt: issue.updatedAt ?? null
260
+ };
261
+ }
262
+ function toIssueSummary(issue) {
263
+ return {
264
+ id: issue.id,
265
+ identifier: issue.identifier,
266
+ title: issue.title,
267
+ url: issue.url,
268
+ priority: issue.priority,
269
+ state: issue.state,
270
+ team: issue.team,
271
+ assignee: issue.assignee,
272
+ createdAt: issue.createdAt,
273
+ updatedAt: issue.updatedAt
274
+ };
275
+ }
276
+ function renderIssueHeadline(issue) {
277
+ const parts = [`${issue.identifier}: ${issue.title}`];
278
+ if (issue.state.name) {
279
+ parts.push(`[${issue.state.name}]`);
280
+ }
281
+ if (issue.assignee) {
282
+ parts.push(`@${issue.assignee.name}`);
283
+ }
284
+ return parts.join(" ");
285
+ }
286
+ function renderSearchIssuesOutput(output) {
287
+ const headerParts = [`Found ${output.total} Linear issue${output.total === 1 ? "" : "s"} for "${output.query}"`];
288
+ if (output.team) {
289
+ headerParts.push(`in ${output.team.name} (${output.team.key})`);
290
+ }
291
+ if (output.stateName) {
292
+ headerParts.push(`with state ${output.stateName}`);
293
+ }
294
+ const lines = [`${headerParts.join(" ")}.`];
295
+ if (output.warning) {
296
+ lines.push(`Warning: ${output.warning}`);
297
+ }
298
+ if (output.total === 0) {
299
+ return lines.join("\n");
300
+ }
301
+ for (const issue of output.issues) {
302
+ lines.push(`- ${renderIssueHeadline(issue)}`);
303
+ }
304
+ return lines.join("\n");
305
+ }
306
+ function renderGetIssueOutput(output) {
307
+ const { issue } = output;
308
+ const lines = [renderIssueHeadline(issue)];
309
+ if (issue.team) {
310
+ lines.push(`Team: ${issue.team.name} (${issue.team.key})`);
311
+ }
312
+ if (issue.url) {
313
+ lines.push(`URL: ${issue.url}`);
314
+ }
315
+ if (issue.branchName) {
316
+ lines.push(`Branch: ${issue.branchName}`);
317
+ }
318
+ if (issue.cycle?.name) {
319
+ lines.push(`Cycle: ${issue.cycle.name}`);
320
+ }
321
+ if (issue.description) {
322
+ lines.push("", issue.description);
323
+ }
324
+ return lines.join("\n");
325
+ }
326
+ function renderCreateIssueOutput(output) {
327
+ const { issue } = output;
328
+ const lines = [`Created Linear issue ${issue.identifier}: ${issue.title}`];
329
+ if (issue.team) {
330
+ lines.push(`Team: ${issue.team.name} (${issue.team.key})`);
331
+ }
332
+ if (issue.url) {
333
+ lines.push(`URL: ${issue.url}`);
334
+ }
335
+ return lines.join("\n");
336
+ }
337
+ var TEAM_FIELDS = `
338
+ id
339
+ key
340
+ name
341
+ `;
342
+ var ISSUE_SUMMARY_FIELDS = `
343
+ id
344
+ identifier
345
+ title
346
+ url
347
+ priority
348
+ createdAt
349
+ updatedAt
350
+ state {
351
+ name
352
+ type
353
+ }
354
+ team {
355
+ ${TEAM_FIELDS}
356
+ }
357
+ assignee {
358
+ id
359
+ name
360
+ }
361
+ `;
362
+ var ISSUE_DETAIL_FIELDS = `
363
+ ${ISSUE_SUMMARY_FIELDS}
364
+ description
365
+ branchName
366
+ cycle {
367
+ id
368
+ number
369
+ name
370
+ startsAt
371
+ endsAt
372
+ }
373
+ `;
374
+ var LIST_TEAMS_QUERY = `
375
+ query ListTeams($first: Int!) {
376
+ teams(first: $first) {
377
+ nodes {
378
+ ${TEAM_FIELDS}
379
+ }
380
+ }
381
+ }
382
+ `;
383
+ var SEARCH_ISSUES_QUERY = `
384
+ query SearchIssues($filter: IssueFilter, $first: Int!) {
385
+ issues(filter: $filter, first: $first, orderBy: updatedAt) {
386
+ nodes {
387
+ ${ISSUE_SUMMARY_FIELDS}
388
+ }
389
+ }
390
+ }
391
+ `;
392
+ var GET_ISSUE_QUERY = `
393
+ query GetIssue($id: String!) {
394
+ issue(id: $id) {
395
+ ${ISSUE_DETAIL_FIELDS}
396
+ }
397
+ }
398
+ `;
399
+ var FIND_ISSUE_BY_IDENTIFIER_QUERY = `
400
+ query FindIssueByIdentifier($filter: IssueFilter, $first: Int!) {
401
+ issues(filter: $filter, first: $first) {
402
+ nodes {
403
+ ${ISSUE_DETAIL_FIELDS}
404
+ }
405
+ }
406
+ }
407
+ `;
408
+ var CREATE_ISSUE_MUTATION = `
409
+ mutation CreateIssue($input: IssueCreateInput!) {
410
+ issueCreate(input: $input) {
411
+ success
412
+ issue {
413
+ ${ISSUE_DETAIL_FIELDS}
414
+ }
415
+ }
416
+ }
417
+ `;
418
+ var listTeamsResponseSchema = z.object({
419
+ teams: z.object({
420
+ nodes: z.array(teamSchema)
421
+ })
422
+ });
423
+ var searchIssuesResponseSchema = z.object({
424
+ issues: z.object({
425
+ nodes: z.array(graphQlIssueSchema)
426
+ })
427
+ });
428
+ var getIssueResponseSchema = z.object({
429
+ issue: graphQlIssueSchema.nullable()
430
+ });
431
+ var createIssueResponseSchema = z.object({
432
+ issueCreate: z.object({
433
+ success: z.boolean(),
434
+ issue: graphQlIssueSchema.nullable()
435
+ })
436
+ });
437
+ var LinearApiClient = class {
438
+ apiKey;
439
+ apiUrl;
440
+ fetchFn;
441
+ constructor(options) {
442
+ this.apiKey = options.apiKey;
443
+ this.apiUrl = options.apiUrl ?? DEFAULT_LINEAR_API_URL;
444
+ this.fetchFn = options.fetchFn ?? fetch;
445
+ }
446
+ async listTeams() {
447
+ const response = await this.request("ListTeams", LIST_TEAMS_QUERY, { first: 100 }, listTeamsResponseSchema);
448
+ return [...response.teams.nodes].sort((left, right) => left.key.localeCompare(right.key));
449
+ }
450
+ async searchIssues(input) {
451
+ const filter = {};
452
+ if (input.teamId) {
453
+ filter.team = { id: { eq: input.teamId } };
454
+ }
455
+ if (input.stateName) {
456
+ filter.state = { name: { eq: input.stateName } };
457
+ }
458
+ if (input.issueNumber !== void 0) {
459
+ filter.number = { eq: input.issueNumber };
460
+ } else {
461
+ filter.title = { containsIgnoreCase: input.query };
462
+ }
463
+ const response = await this.request("SearchIssues", SEARCH_ISSUES_QUERY, { filter, first: input.limit }, searchIssuesResponseSchema);
464
+ return response.issues.nodes.map((issue) => toIssueSummary(normalizeIssue(issue)));
465
+ }
466
+ async getIssueById(id) {
467
+ const response = await this.request("GetIssue", GET_ISSUE_QUERY, { id }, getIssueResponseSchema);
468
+ if (!response.issue) {
469
+ throw new ExternalServiceError(`Linear issue '${id}' was not found.`, {
470
+ statusCode: 404,
471
+ details: { id }
472
+ });
473
+ }
474
+ return normalizeIssue(response.issue);
475
+ }
476
+ async getIssueByIdentifier(teamId, issueNumber) {
477
+ const filter = {
478
+ team: { id: { eq: teamId } },
479
+ number: { eq: issueNumber }
480
+ };
481
+ const response = await this.request(
482
+ "FindIssueByIdentifier",
483
+ FIND_ISSUE_BY_IDENTIFIER_QUERY,
484
+ { filter, first: 1 },
485
+ searchIssuesResponseSchema
486
+ );
487
+ const issue = response.issues.nodes[0];
488
+ if (!issue) {
489
+ throw new ExternalServiceError(`Linear issue '${issueNumber}' was not found for team '${teamId}'.`, {
490
+ statusCode: 404,
491
+ details: { teamId, issueNumber }
492
+ });
493
+ }
494
+ return normalizeIssue(issue);
495
+ }
496
+ async createIssue(input) {
497
+ const mutationInput = {
498
+ title: input.title,
499
+ teamId: input.teamId
500
+ };
501
+ if (input.description !== void 0) {
502
+ mutationInput.description = input.description;
503
+ }
504
+ if (input.priority !== void 0) {
505
+ mutationInput.priority = input.priority;
506
+ }
507
+ if (input.stateId !== void 0) {
508
+ mutationInput.stateId = input.stateId;
509
+ }
510
+ if (input.assigneeId !== void 0) {
511
+ mutationInput.assigneeId = input.assigneeId;
512
+ }
513
+ if (input.labelIds !== void 0) {
514
+ mutationInput.labelIds = input.labelIds;
515
+ }
516
+ if (input.dueDate !== void 0) {
517
+ mutationInput.dueDate = input.dueDate;
518
+ }
519
+ if (input.projectId !== void 0) {
520
+ mutationInput.projectId = input.projectId;
521
+ }
522
+ if (input.cycleId !== void 0) {
523
+ mutationInput.cycleId = input.cycleId;
524
+ }
525
+ const response = await this.request("CreateIssue", CREATE_ISSUE_MUTATION, { input: mutationInput }, createIssueResponseSchema);
526
+ if (!response.issueCreate.success || !response.issueCreate.issue) {
527
+ throw new ExternalServiceError("Linear did not confirm that the issue was created.", {
528
+ statusCode: 502,
529
+ details: response.issueCreate
530
+ });
531
+ }
532
+ return normalizeIssue(response.issueCreate.issue);
533
+ }
534
+ async request(operationName, query, variables, dataSchema) {
535
+ let response;
536
+ try {
537
+ response = await this.fetchFn(this.apiUrl, {
538
+ method: "POST",
539
+ headers: {
540
+ authorization: `Bearer ${this.apiKey}`,
541
+ "content-type": "application/json"
542
+ },
543
+ body: JSON.stringify({ query, variables })
544
+ });
545
+ } catch (error) {
546
+ const normalized = normalizeError(error);
547
+ throw new ExternalServiceError(`Could not reach the Linear API while running ${operationName}.`, {
548
+ statusCode: 502,
549
+ details: {
550
+ message: normalized.message,
551
+ details: normalized.details
552
+ }
553
+ });
554
+ }
555
+ const rawBody = await response.text();
556
+ let payload = {};
557
+ if (rawBody.length > 0) {
558
+ try {
559
+ payload = JSON.parse(rawBody);
560
+ } catch {
561
+ throw new ExternalServiceError(`Linear returned invalid JSON for ${operationName}.`, {
562
+ statusCode: response.status || 502,
563
+ details: rawBody.slice(0, 2e3)
564
+ });
565
+ }
566
+ }
567
+ const looseErrorEnvelope = createGraphQlEnvelopeSchema(z.unknown());
568
+ const parsedLooseEnvelope = looseErrorEnvelope.safeParse(payload);
569
+ if (!response.ok) {
570
+ if (parsedLooseEnvelope.success && parsedLooseEnvelope.data.errors?.length) {
571
+ throw buildGraphQlError(operationName, parsedLooseEnvelope.data.errors, response.status);
572
+ }
573
+ throw new ExternalServiceError(`Linear returned HTTP ${response.status} for ${operationName}.`, {
574
+ statusCode: response.status,
575
+ details: payload
576
+ });
577
+ }
578
+ const envelopeSchema = createGraphQlEnvelopeSchema(dataSchema);
579
+ const parsedEnvelope = envelopeSchema.safeParse(payload);
580
+ if (!parsedEnvelope.success) {
581
+ throw new ExternalServiceError(`Linear returned an unexpected payload for ${operationName}.`, {
582
+ details: parsedEnvelope.error.flatten()
583
+ });
584
+ }
585
+ if (parsedEnvelope.data.errors?.length) {
586
+ throw buildGraphQlError(operationName, parsedEnvelope.data.errors);
587
+ }
588
+ if (parsedEnvelope.data.data === void 0) {
589
+ throw new ExternalServiceError(`Linear returned no data for ${operationName}.`, {
590
+ details: payload
591
+ });
592
+ }
593
+ return parsedEnvelope.data.data;
594
+ }
595
+ };
596
+ var metadata = {
597
+ id: "linear",
598
+ title: "Linear MCP Server",
599
+ description: "Search, inspect, create, and triage Linear issues with team resources and prompts.",
600
+ version: "0.1.0",
601
+ packageName: "@universal-mcp-toolkit/server-linear",
602
+ homepage: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit#readme",
603
+ repositoryUrl: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit",
604
+ documentationUrl: "https://developers.linear.app/docs/graphql",
605
+ envVarNames: [
606
+ "LINEAR_API_KEY",
607
+ "LINEAR_DEFAULT_TEAM_ID",
608
+ "LINEAR_DEFAULT_TEAM_KEY",
609
+ "LINEAR_WORKSPACE_NAME",
610
+ "LINEAR_API_URL"
611
+ ],
612
+ transports: ["stdio", "sse"],
613
+ toolNames: TOOL_NAMES,
614
+ resourceNames: RESOURCE_NAMES,
615
+ promptNames: PROMPT_NAMES
616
+ };
617
+ var serverCard = createServerCard(metadata);
618
+ var LinearServer = class extends ToolkitServer {
619
+ client;
620
+ defaultTeamId;
621
+ defaultTeamKey;
622
+ workspaceName;
623
+ constructor(options) {
624
+ super(metadata);
625
+ this.client = options.client;
626
+ this.defaultTeamId = options.defaultTeamId ?? null;
627
+ this.defaultTeamKey = normalizeTeamKey(options.defaultTeamKey) ?? null;
628
+ this.workspaceName = options.workspaceName ?? null;
629
+ this.registerSearchIssuesTool();
630
+ this.registerGetIssueTool();
631
+ this.registerCreateIssueTool();
632
+ this.registerTeamResource();
633
+ this.registerSprintTriagePrompt();
634
+ }
635
+ async getTeamResourcePayload() {
636
+ return this.runOperation("load the Linear team resource", async () => {
637
+ const selection = await this.resolveTeamSelection({
638
+ useDefaults: true
639
+ });
640
+ return {
641
+ workspaceName: this.workspaceName,
642
+ configuredDefaultTeamId: this.defaultTeamId,
643
+ configuredDefaultTeamKey: this.defaultTeamKey,
644
+ defaultTeam: selection.team,
645
+ accessibleTeams: selection.teams,
646
+ warning: selection.warning
647
+ };
648
+ });
649
+ }
650
+ async createSprintTriagePrompt(args = {}) {
651
+ return this.runOperation("create the sprint triage prompt", async () => {
652
+ const selectionOptions = {
653
+ useDefaults: true,
654
+ strictOnMissing: args.teamId !== void 0 || args.teamKey !== void 0,
655
+ ...withOptionalProperty("teamId", args.teamId),
656
+ ...withOptionalProperty("teamKey", args.teamKey)
657
+ };
658
+ const selection = await this.resolveTeamSelection(selectionOptions);
659
+ const issueLimit = args.issueLimit ?? 10;
660
+ const includeBacklog = args.includeBacklog ?? true;
661
+ const selectedTeamLabel = selection.team ? `${selection.team.name} (${selection.team.key})` : "No default team selected";
662
+ const accessibleTeams = selection.teams.length > 0 ? selection.teams.map((team) => `${team.name} (${team.key})`).join(", ") : "No teams were returned for this API token.";
663
+ const text = [
664
+ `You are preparing sprint triage for the Linear workspace ${this.workspaceName ?? "associated with the current API token"}.`,
665
+ `Primary team context: ${selectedTeamLabel}.`,
666
+ `Accessible teams: ${accessibleTeams}`,
667
+ `Objective: ${args.objective ?? "Review the next sprint, identify the right scope, and flag any missing work."}`,
668
+ `Focus area: ${args.focus ?? "overall sprint health, priorities, and blockers"}.`,
669
+ `Review up to ${issueLimit} issues.`,
670
+ includeBacklog ? "Include realistic backlog carry-over items if they are important for the next sprint." : "Exclude backlog carry-over items unless they are direct blockers for sprint goals.",
671
+ "Use the available Linear capabilities in this order:",
672
+ "1. Read the team resource at linear://team/default for team context.",
673
+ "2. Use search_issues to gather candidate sprint work.",
674
+ "3. Use get_issue for any issue that needs deeper inspection.",
675
+ "4. Suggest create_issue actions for gaps, follow-up work, or missing dependencies.",
676
+ "Return the triage plan grouped into: must ship, stretch, defer, blockers, and missing issues.",
677
+ selection.warning ? `Warning: ${selection.warning}` : void 0,
678
+ "Current team snapshot:",
679
+ JSON.stringify(
680
+ {
681
+ workspaceName: this.workspaceName,
682
+ selectedTeam: selection.team,
683
+ accessibleTeams: selection.teams
684
+ },
685
+ null,
686
+ 2
687
+ )
688
+ ].filter((line) => line !== void 0).join("\n");
689
+ return {
690
+ messages: [
691
+ {
692
+ role: "user",
693
+ content: {
694
+ type: "text",
695
+ text
696
+ }
697
+ }
698
+ ]
699
+ };
700
+ });
701
+ }
702
+ registerSearchIssuesTool() {
703
+ this.registerTool(
704
+ defineTool({
705
+ name: "search_issues",
706
+ title: "Search Linear issues",
707
+ description: "Search Linear issues by title or exact issue identifier, optionally scoped to a team or state.",
708
+ inputSchema: searchIssuesInputShape,
709
+ outputSchema: searchIssuesOutputShape,
710
+ handler: async (input, context) => this.runOperation("search Linear issues", async () => {
711
+ const identifier = parseIssueIdentifier(input.query);
712
+ const requestedTeamKey = input.teamKey;
713
+ if (identifier && requestedTeamKey && normalizeTeamKey(requestedTeamKey) !== identifier.teamKey) {
714
+ throw new ValidationError(
715
+ `The issue identifier '${input.query}' targets team '${identifier.teamKey}', which does not match teamKey '${requestedTeamKey}'.`
716
+ );
717
+ }
718
+ await context.log("info", `Searching Linear issues for '${input.query}'.`);
719
+ const selectionOptions = {
720
+ useDefaults: input.teamId === void 0 && requestedTeamKey === void 0 && identifier === null,
721
+ strictOnMissing: input.teamId !== void 0 || requestedTeamKey !== void 0 || identifier !== null,
722
+ ...withOptionalProperty("teamId", input.teamId),
723
+ ...withOptionalProperty("teamKey", requestedTeamKey ?? identifier?.teamKey)
724
+ };
725
+ const selection = await this.resolveTeamSelection(selectionOptions);
726
+ const searchRequest = {
727
+ query: input.query,
728
+ limit: input.limit,
729
+ ...withOptionalProperty("stateName", input.stateName),
730
+ ...withOptionalProperty("teamId", selection.team?.id),
731
+ ...withOptionalProperty("issueNumber", identifier?.number)
732
+ };
733
+ const issues = await this.client.searchIssues(searchRequest);
734
+ await context.log("info", `Linear returned ${issues.length} issue(s).`);
735
+ return {
736
+ query: input.query,
737
+ stateName: input.stateName ?? null,
738
+ team: selection.team,
739
+ warning: selection.warning,
740
+ total: issues.length,
741
+ issues
742
+ };
743
+ }),
744
+ renderText: renderSearchIssuesOutput
745
+ })
746
+ );
747
+ }
748
+ registerGetIssueTool() {
749
+ this.registerTool(
750
+ defineTool({
751
+ name: "get_issue",
752
+ title: "Get a Linear issue",
753
+ description: "Get a Linear issue by UUID/id or by issue identifier such as ENG-123.",
754
+ inputSchema: getIssueInputShape,
755
+ outputSchema: getIssueOutputShape,
756
+ handler: async (input, context) => this.runOperation("get a Linear issue", async () => {
757
+ await context.log("info", `Loading Linear issue '${input.idOrIdentifier}'.`);
758
+ const parsedIdentifier = parseIssueIdentifier(input.idOrIdentifier);
759
+ const issue = parsedIdentifier === null ? await this.client.getIssueById(input.idOrIdentifier) : await this.getIssueByTeamKeyAndNumber(parsedIdentifier.teamKey, parsedIdentifier.number);
760
+ await context.log("info", `Loaded Linear issue '${issue.identifier}'.`);
761
+ return { issue };
762
+ }),
763
+ renderText: renderGetIssueOutput
764
+ })
765
+ );
766
+ }
767
+ registerCreateIssueTool() {
768
+ this.registerTool(
769
+ defineTool({
770
+ name: "create_issue",
771
+ title: "Create a Linear issue",
772
+ description: "Create a new Linear issue using an explicit team or the configured default team.",
773
+ inputSchema: createIssueInputShape,
774
+ outputSchema: createIssueOutputShape,
775
+ handler: async (input, context) => this.runOperation("create a Linear issue", async () => {
776
+ const selectionOptions = {
777
+ useDefaults: true,
778
+ requireResolved: true,
779
+ strictOnMissing: true,
780
+ ...withOptionalProperty("teamId", input.teamId),
781
+ ...withOptionalProperty("teamKey", input.teamKey)
782
+ };
783
+ const selection = await this.resolveTeamSelection(selectionOptions);
784
+ if (!selection.team) {
785
+ throw new ConfigurationError(
786
+ "A Linear team is required to create issues. Provide teamId/teamKey or configure LINEAR_DEFAULT_TEAM_ID or LINEAR_DEFAULT_TEAM_KEY."
787
+ );
788
+ }
789
+ await context.log("info", `Creating a Linear issue in team '${selection.team.key}'.`);
790
+ const createIssueInput = {
791
+ teamId: selection.team.id,
792
+ title: input.title,
793
+ ...withOptionalProperty("description", input.description),
794
+ ...withOptionalProperty("priority", input.priority),
795
+ ...withOptionalProperty("stateId", input.stateId),
796
+ ...withOptionalProperty("assigneeId", input.assigneeId),
797
+ ...withOptionalProperty("labelIds", input.labelIds),
798
+ ...withOptionalProperty("dueDate", input.dueDate),
799
+ ...withOptionalProperty("projectId", input.projectId),
800
+ ...withOptionalProperty("cycleId", input.cycleId)
801
+ };
802
+ const issue = await this.client.createIssue(createIssueInput);
803
+ await context.log("info", `Created Linear issue '${issue.identifier}'.`);
804
+ return {
805
+ created: true,
806
+ issue
807
+ };
808
+ }),
809
+ renderText: renderCreateIssueOutput
810
+ })
811
+ );
812
+ }
813
+ registerTeamResource() {
814
+ this.registerStaticResource(
815
+ "team",
816
+ TEAM_RESOURCE_URI,
817
+ {
818
+ title: "Linear team context",
819
+ description: "JSON summary of accessible Linear teams and the configured default team.",
820
+ mimeType: "application/json"
821
+ },
822
+ async (uri) => this.createJsonResource(uri.toString(), await this.getTeamResourcePayload())
823
+ );
824
+ }
825
+ registerSprintTriagePrompt() {
826
+ this.registerPrompt(
827
+ "sprint-triage",
828
+ {
829
+ title: "Sprint triage",
830
+ description: "Create a sprint triage prompt grounded in the current Linear team context.",
831
+ argsSchema: sprintTriagePromptArgsShape
832
+ },
833
+ async (args) => this.createSprintTriagePrompt(args)
834
+ );
835
+ }
836
+ async getIssueByTeamKeyAndNumber(teamKey, issueNumber) {
837
+ const selection = await this.resolveTeamSelection({
838
+ teamKey,
839
+ requireResolved: true,
840
+ strictOnMissing: true
841
+ });
842
+ if (!selection.team) {
843
+ throw new ExternalServiceError(`Linear team '${teamKey}' was not found.`, {
844
+ statusCode: 404,
845
+ details: { teamKey }
846
+ });
847
+ }
848
+ return this.client.getIssueByIdentifier(selection.team.id, issueNumber);
849
+ }
850
+ async resolveTeamSelection(options) {
851
+ const teams = await this.client.listTeams();
852
+ const requestedTeamId = options.teamId ?? (options.useDefaults ? this.defaultTeamId ?? void 0 : void 0);
853
+ const requestedTeamKey = normalizeTeamKey(options.teamKey) ?? (options.useDefaults ? this.defaultTeamKey ?? void 0 : void 0);
854
+ const usingDefaultSelection = options.useDefaults === true && options.teamId === void 0 && options.teamKey === void 0;
855
+ const byId = requestedTeamId ? teams.find((team2) => team2.id === requestedTeamId) ?? null : null;
856
+ const byKey = requestedTeamKey ? teams.find((team2) => normalizeTeamKey(team2.key) === requestedTeamKey) ?? null : null;
857
+ if (requestedTeamId && requestedTeamKey) {
858
+ const mismatch = !byId || !byKey || byId.id !== byKey.id;
859
+ if (mismatch) {
860
+ if (usingDefaultSelection) {
861
+ throw new ConfigurationError(
862
+ "LINEAR_DEFAULT_TEAM_ID and LINEAR_DEFAULT_TEAM_KEY do not resolve to the same accessible Linear team.",
863
+ {
864
+ requestedTeamId,
865
+ requestedTeamKey
866
+ }
867
+ );
868
+ }
869
+ throw new ValidationError("teamId and teamKey must refer to the same Linear team.");
870
+ }
871
+ }
872
+ const team = byId ?? byKey ?? null;
873
+ if (team) {
874
+ return {
875
+ teams,
876
+ team,
877
+ warning: null
878
+ };
879
+ }
880
+ const requestedValue = requestedTeamId ?? requestedTeamKey;
881
+ if (!requestedValue) {
882
+ return {
883
+ teams,
884
+ team: null,
885
+ warning: null
886
+ };
887
+ }
888
+ if (options.requireResolved || options.strictOnMissing) {
889
+ throw new ExternalServiceError(`Linear team '${requestedValue}' was not found.`, {
890
+ statusCode: 404,
891
+ details: {
892
+ requestedTeamId,
893
+ requestedTeamKey
894
+ }
895
+ });
896
+ }
897
+ return {
898
+ teams,
899
+ team: null,
900
+ warning: usingDefaultSelection ? "The configured default Linear team could not be resolved with the current API token." : `Linear team '${requestedValue}' was not found.`
901
+ };
902
+ }
903
+ async runOperation(operation, work) {
904
+ try {
905
+ return await work();
906
+ } catch (error) {
907
+ if (error instanceof ConfigurationError || error instanceof ValidationError) {
908
+ throw error;
909
+ }
910
+ if (error instanceof ExternalServiceError) {
911
+ throw new ExternalServiceError(`Unable to ${operation}. ${error.message}`, {
912
+ statusCode: error.statusCode,
913
+ details: error.details,
914
+ exposeToClient: error.exposeToClient
915
+ });
916
+ }
917
+ const normalized = normalizeError(error);
918
+ throw new ExternalServiceError(`Unable to ${operation}. ${normalized.message}`, {
919
+ statusCode: normalized.statusCode,
920
+ details: normalized.details,
921
+ exposeToClient: normalized.exposeToClient
922
+ });
923
+ }
924
+ }
925
+ };
926
+ function loadLinearEnvironment(source = process.env) {
927
+ const env = loadEnv(linearEnvShape, source);
928
+ const defaultTeamKey = normalizeTeamKey(env.LINEAR_DEFAULT_TEAM_KEY);
929
+ return {
930
+ LINEAR_API_KEY: env.LINEAR_API_KEY,
931
+ LINEAR_API_URL: env.LINEAR_API_URL ?? DEFAULT_LINEAR_API_URL,
932
+ ...withOptionalProperty("LINEAR_DEFAULT_TEAM_ID", env.LINEAR_DEFAULT_TEAM_ID),
933
+ ...withOptionalProperty("LINEAR_DEFAULT_TEAM_KEY", defaultTeamKey),
934
+ ...withOptionalProperty("LINEAR_WORKSPACE_NAME", env.LINEAR_WORKSPACE_NAME)
935
+ };
936
+ }
937
+ async function createServer(options = {}) {
938
+ const environment = options.environment ?? loadLinearEnvironment();
939
+ const client = options.client ?? new LinearApiClient({
940
+ apiKey: environment.LINEAR_API_KEY,
941
+ apiUrl: environment.LINEAR_API_URL,
942
+ ...withOptionalProperty("fetchFn", options.fetchFn)
943
+ });
944
+ return new LinearServer({
945
+ client,
946
+ defaultTeamId: environment.LINEAR_DEFAULT_TEAM_ID ?? null,
947
+ defaultTeamKey: environment.LINEAR_DEFAULT_TEAM_KEY ?? null,
948
+ workspaceName: environment.LINEAR_WORKSPACE_NAME ?? null
949
+ });
950
+ }
951
+ var runtimeRegistration = {
952
+ createServer: () => createServer(),
953
+ serverCard
954
+ };
955
+ async function main() {
956
+ const runtimeOptions = parseRuntimeOptions();
957
+ await runToolkitServer(runtimeRegistration, runtimeOptions);
958
+ }
959
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
960
+ void main().catch((error) => {
961
+ const normalized = normalizeError(error);
962
+ console.error(normalized.toClientMessage());
963
+ process.exitCode = 1;
964
+ });
965
+ }
966
+ export {
967
+ LinearApiClient,
968
+ LinearServer,
969
+ PROMPT_NAMES,
970
+ RESOURCE_NAMES,
971
+ TEAM_RESOURCE_URI,
972
+ TOOL_NAMES,
973
+ createIssueInputShape,
974
+ createIssueOutputShape,
975
+ createServer,
976
+ getIssueInputShape,
977
+ getIssueOutputShape,
978
+ loadLinearEnvironment,
979
+ main,
980
+ metadata,
981
+ searchIssuesInputShape,
982
+ searchIssuesOutputShape,
983
+ serverCard,
984
+ sprintTriagePromptArgsShape
985
+ };