@striderlabs/mcp-linear 1.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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { z } from "zod";
11
+ var API_KEY = process.env.LINEAR_API_KEY;
12
+ var GRAPHQL_URL = "https://api.linear.app/graphql";
13
+ async function linearQuery(query, variables) {
14
+ const headers = {
15
+ "Content-Type": "application/json"
16
+ };
17
+ if (API_KEY) {
18
+ headers["Authorization"] = API_KEY;
19
+ }
20
+ const res = await fetch(GRAPHQL_URL, {
21
+ method: "POST",
22
+ headers,
23
+ body: JSON.stringify({ query, variables })
24
+ });
25
+ const data = await res.json();
26
+ if (!res.ok) {
27
+ throw new Error(`Linear API error ${res.status}: ${JSON.stringify(data)}`);
28
+ }
29
+ if (data.errors) {
30
+ throw new Error(`Linear GraphQL errors: ${JSON.stringify(data.errors)}`);
31
+ }
32
+ return data.data;
33
+ }
34
+ var server = new Server(
35
+ { name: "mcp-linear", version: "1.0.0" },
36
+ { capabilities: { tools: {} } }
37
+ );
38
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
39
+ tools: [
40
+ {
41
+ name: "list_issues",
42
+ description: "List Linear issues",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ team_id: { type: "string", description: "Filter by team ID" },
47
+ first: { type: "number", description: "Number of issues to return (default 20)" },
48
+ state: { type: "string", description: "Filter by state name (e.g. 'In Progress')" },
49
+ assignee_id: { type: "string", description: "Filter by assignee ID" }
50
+ }
51
+ }
52
+ },
53
+ {
54
+ name: "get_issue",
55
+ description: "Get a Linear issue by ID",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ issue_id: { type: "string", description: "The issue ID" }
60
+ },
61
+ required: ["issue_id"]
62
+ }
63
+ },
64
+ {
65
+ name: "create_issue",
66
+ description: "Create a new Linear issue",
67
+ inputSchema: {
68
+ type: "object",
69
+ properties: {
70
+ title: { type: "string", description: "Issue title" },
71
+ description: { type: "string", description: "Issue description (markdown)" },
72
+ team_id: { type: "string", description: "Team ID" },
73
+ assignee_id: { type: "string", description: "Assignee user ID" },
74
+ priority: { type: "number", description: "Priority 0-4 (0=no priority, 1=urgent, 2=high, 3=medium, 4=low)" },
75
+ state_id: { type: "string", description: "Workflow state ID" },
76
+ label_ids: { type: "array", items: { type: "string" }, description: "Label IDs" }
77
+ },
78
+ required: ["title", "team_id"]
79
+ }
80
+ },
81
+ {
82
+ name: "update_issue",
83
+ description: "Update a Linear issue",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: {
87
+ issue_id: { type: "string", description: "The issue ID" },
88
+ title: { type: "string", description: "New title" },
89
+ description: { type: "string", description: "New description" },
90
+ state_id: { type: "string", description: "New state ID" },
91
+ assignee_id: { type: "string", description: "New assignee ID" },
92
+ priority: { type: "number", description: "New priority (0-4)" }
93
+ },
94
+ required: ["issue_id"]
95
+ }
96
+ },
97
+ {
98
+ name: "list_projects",
99
+ description: "List Linear projects",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ team_id: { type: "string", description: "Filter by team ID" },
104
+ first: { type: "number", description: "Number of projects to return" }
105
+ }
106
+ }
107
+ },
108
+ {
109
+ name: "search_issues",
110
+ description: "Search Linear issues by text",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ query: { type: "string", description: "Search query" },
115
+ first: { type: "number", description: "Number of results" }
116
+ },
117
+ required: ["query"]
118
+ }
119
+ }
120
+ ]
121
+ }));
122
+ var ListIssuesSchema = z.object({
123
+ team_id: z.string().optional(),
124
+ first: z.number().optional(),
125
+ state: z.string().optional(),
126
+ assignee_id: z.string().optional()
127
+ });
128
+ var GetIssueSchema = z.object({ issue_id: z.string() });
129
+ var CreateIssueSchema = z.object({
130
+ title: z.string(),
131
+ description: z.string().optional(),
132
+ team_id: z.string(),
133
+ assignee_id: z.string().optional(),
134
+ priority: z.number().optional(),
135
+ state_id: z.string().optional(),
136
+ label_ids: z.array(z.string()).optional()
137
+ });
138
+ var UpdateIssueSchema = z.object({
139
+ issue_id: z.string(),
140
+ title: z.string().optional(),
141
+ description: z.string().optional(),
142
+ state_id: z.string().optional(),
143
+ assignee_id: z.string().optional(),
144
+ priority: z.number().optional()
145
+ });
146
+ var ListProjectsSchema = z.object({
147
+ team_id: z.string().optional(),
148
+ first: z.number().optional()
149
+ });
150
+ var SearchIssuesSchema = z.object({
151
+ query: z.string(),
152
+ first: z.number().optional()
153
+ });
154
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
155
+ const { name, arguments: args } = request.params;
156
+ try {
157
+ switch (name) {
158
+ case "list_issues": {
159
+ const params = ListIssuesSchema.parse(args);
160
+ const filter = {};
161
+ if (params.team_id) filter.team = { id: { eq: params.team_id } };
162
+ if (params.state) filter.state = { name: { eq: params.state } };
163
+ if (params.assignee_id) filter.assignee = { id: { eq: params.assignee_id } };
164
+ const query = `
165
+ query ListIssues($first: Int, $filter: IssueFilter) {
166
+ issues(first: $first, filter: $filter) {
167
+ nodes {
168
+ id
169
+ title
170
+ description
171
+ priority
172
+ state { id name }
173
+ assignee { id name email }
174
+ team { id name }
175
+ createdAt
176
+ updatedAt
177
+ }
178
+ }
179
+ }
180
+ `;
181
+ const result = await linearQuery(query, {
182
+ first: params.first ?? 20,
183
+ filter: Object.keys(filter).length > 0 ? filter : void 0
184
+ });
185
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
186
+ }
187
+ case "get_issue": {
188
+ const { issue_id } = GetIssueSchema.parse(args);
189
+ const query = `
190
+ query GetIssue($id: String!) {
191
+ issue(id: $id) {
192
+ id
193
+ title
194
+ description
195
+ priority
196
+ state { id name }
197
+ assignee { id name email }
198
+ team { id name }
199
+ labels { nodes { id name } }
200
+ comments { nodes { id body createdAt user { name } } }
201
+ createdAt
202
+ updatedAt
203
+ }
204
+ }
205
+ `;
206
+ const result = await linearQuery(query, { id: issue_id });
207
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
208
+ }
209
+ case "create_issue": {
210
+ const params = CreateIssueSchema.parse(args);
211
+ const mutation = `
212
+ mutation CreateIssue($input: IssueCreateInput!) {
213
+ issueCreate(input: $input) {
214
+ success
215
+ issue {
216
+ id
217
+ title
218
+ state { name }
219
+ team { name }
220
+ }
221
+ }
222
+ }
223
+ `;
224
+ const input = {
225
+ title: params.title,
226
+ teamId: params.team_id
227
+ };
228
+ if (params.description) input.description = params.description;
229
+ if (params.assignee_id) input.assigneeId = params.assignee_id;
230
+ if (params.priority !== void 0) input.priority = params.priority;
231
+ if (params.state_id) input.stateId = params.state_id;
232
+ if (params.label_ids) input.labelIds = params.label_ids;
233
+ const result = await linearQuery(mutation, { input });
234
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
235
+ }
236
+ case "update_issue": {
237
+ const { issue_id, ...updates } = UpdateIssueSchema.parse(args);
238
+ const mutation = `
239
+ mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
240
+ issueUpdate(id: $id, input: $input) {
241
+ success
242
+ issue {
243
+ id
244
+ title
245
+ state { name }
246
+ }
247
+ }
248
+ }
249
+ `;
250
+ const input = {};
251
+ if (updates.title) input.title = updates.title;
252
+ if (updates.description) input.description = updates.description;
253
+ if (updates.state_id) input.stateId = updates.state_id;
254
+ if (updates.assignee_id) input.assigneeId = updates.assignee_id;
255
+ if (updates.priority !== void 0) input.priority = updates.priority;
256
+ const result = await linearQuery(mutation, { id: issue_id, input });
257
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
258
+ }
259
+ case "list_projects": {
260
+ const params = ListProjectsSchema.parse(args);
261
+ const filter = {};
262
+ if (params.team_id) filter.accessibleTeams = { some: { id: { eq: params.team_id } } };
263
+ const query = `
264
+ query ListProjects($first: Int, $filter: ProjectFilter) {
265
+ projects(first: $first, filter: $filter) {
266
+ nodes {
267
+ id
268
+ name
269
+ description
270
+ state
271
+ startDate
272
+ targetDate
273
+ teams { nodes { id name } }
274
+ }
275
+ }
276
+ }
277
+ `;
278
+ const result = await linearQuery(query, {
279
+ first: params.first ?? 20,
280
+ filter: Object.keys(filter).length > 0 ? filter : void 0
281
+ });
282
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
283
+ }
284
+ case "search_issues": {
285
+ const { query: searchQuery, first } = SearchIssuesSchema.parse(args);
286
+ const query = `
287
+ query SearchIssues($query: String!, $first: Int) {
288
+ issueSearch(query: $query, first: $first) {
289
+ nodes {
290
+ id
291
+ title
292
+ description
293
+ priority
294
+ state { name }
295
+ assignee { name }
296
+ team { name }
297
+ }
298
+ }
299
+ }
300
+ `;
301
+ const result = await linearQuery(query, { query: searchQuery, first: first ?? 20 });
302
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
303
+ }
304
+ default:
305
+ throw new Error(`Unknown tool: ${name}`);
306
+ }
307
+ } catch (error) {
308
+ return {
309
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
310
+ isError: true
311
+ };
312
+ }
313
+ });
314
+ var transport = new StdioServerTransport();
315
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@striderlabs/mcp-linear",
3
+ "version": "1.0.0",
4
+ "description": "MCP connector for Linear GraphQL API",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-linear": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --dts",
12
+ "dev": "tsup src/index.ts --format esm --watch"
13
+ },
14
+ "files": ["dist"],
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "latest",
17
+ "zod": "latest"
18
+ },
19
+ "devDependencies": {
20
+ "tsup": "latest",
21
+ "typescript": "latest",
22
+ "@types/node": "latest"
23
+ }
24
+ }