@sentry/junior-linear 0.122.0 → 0.123.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/README.md CHANGED
@@ -19,7 +19,7 @@ export const plugins = defineJuniorPlugins([linearPlugin()]);
19
19
 
20
20
  This package does not require a shared `LINEAR_API_KEY` or a custom OAuth app for the default setup. Each user connects their own Linear account the first time Junior calls a Linear MCP tool. Junior sends the authorization link privately and resumes the same Slack thread automatically after the user authorizes.
21
21
 
22
- Issue creation uses a Junior-owned tool backed by Linear's hosted MCP connection. This lets Junior link created issues to the conversation while keeping Linear's existing OAuth flow. Other Linear operations continue to use the hosted MCP tools.
22
+ Linear operations use Linear's hosted MCP tools directly. When an issue is created through that path, Junior links it to the current conversation.
23
23
 
24
24
  Optional: set channel defaults when a Slack thread usually routes work to the same Linear destination:
25
25
 
package/dist/index.js CHANGED
@@ -2,245 +2,36 @@
2
2
  import {
3
3
  defineJuniorPlugin
4
4
  } from "@sentry/junior-plugin-api";
5
-
6
- // src/tools/create-issue.ts
7
- import {
8
- definePluginTool,
9
- pluginToolResultSchema
10
- } from "@sentry/junior-plugin-api";
11
5
  import { z } from "zod";
12
- var inputSchema = z.object({
13
- team: z.string().trim().min(1).describe("Linear team name or identifier."),
14
- title: z.string().trim().min(1).max(60).describe("Issue title."),
15
- description: z.string().describe("Issue description.").optional(),
16
- priority: z.enum(["low", "medium", "high", "urgent"]).describe("Linear priority.").optional(),
17
- project: z.string().trim().min(1).describe("Linear project name or identifier.").optional()
18
- }).strict();
19
- var issueSchema = z.object({
20
- id: z.string().optional(),
21
- identifier: z.string().optional(),
22
- title: z.string().optional(),
23
- url: z.string().url().optional()
24
- }).strict();
25
- var outputSchema = pluginToolResultSchema.extend({
26
- ok: z.literal(true),
27
- status: z.literal("success"),
28
- target: z.literal("createIssue"),
29
- data: z.object({
30
- issue: issueSchema.nullable()
31
- }),
32
- issue: issueSchema.nullable()
33
- });
34
- function collectObjects(value, objects) {
35
- if (!value || typeof value !== "object") {
6
+ var saveIssueResultSchema = z.object({
7
+ issue: z.object({
8
+ identifier: z.string().trim().min(1),
9
+ url: z.url()
10
+ }).passthrough()
11
+ }).passthrough();
12
+ async function annotateCreatedIssue(ctx) {
13
+ if (ctx.tool.name !== "save_issue" || ctx.tool.arguments.id !== void 0) {
36
14
  return;
37
15
  }
38
- if (Array.isArray(value)) {
39
- for (const item of value) collectObjects(item, objects);
16
+ if (!ctx.annotations) {
40
17
  return;
41
18
  }
42
- const record = value;
43
- objects.push(record);
44
- for (const item of Object.values(record)) collectObjects(item, objects);
45
- }
46
- function stringField(objects, field) {
47
- for (const object of objects) {
48
- const value = object[field];
49
- if (typeof value === "string" && value.trim()) return value.trim();
50
- }
51
- return void 0;
52
- }
53
- function parseJsonText(text) {
54
- try {
55
- return JSON.parse(text);
56
- } catch {
57
- return void 0;
58
- }
59
- }
60
- function extractIssue(result) {
61
- const objects = [];
62
- collectObjects(result.structuredContent, objects);
63
- const textParts = result.content.flatMap(
64
- (part) => part.type === "text" ? [part.text] : []
65
- );
66
- for (const text of textParts) {
67
- collectObjects(parseJsonText(text), objects);
68
- }
69
- const urlMatch = textParts.map(
70
- (text) => text.match(
71
- /https:\/\/linear\.app\/[^\s<>)"']+\/issue\/([A-Z][A-Z0-9]*-\d+)[^\s<>)"']*/i
72
- )
73
- ).find((match) => match !== null);
74
- const identifierMatch = textParts.map((text) => text.match(/\b[A-Z][A-Z0-9]*-\d+\b/)).find((match) => match !== null);
75
- const issue = {
76
- id: stringField(objects, "id"),
77
- identifier: stringField(objects, "identifier") ?? urlMatch?.[1]?.toUpperCase() ?? identifierMatch?.[0],
78
- title: stringField(objects, "title"),
79
- url: stringField(objects, "url") ?? urlMatch?.[0]
80
- };
81
- const parsed = issueSchema.safeParse(
82
- Object.fromEntries(
83
- Object.entries(issue).filter((entry) => entry[1] !== void 0)
84
- )
85
- );
86
- if (!parsed.success || Object.keys(parsed.data).length === 0) {
87
- return null;
88
- }
89
- return parsed.data;
90
- }
91
- function toolResult(issue, content) {
92
- const data = { issue };
93
- return {
94
- content,
95
- details: {
96
- ok: true,
97
- status: "success",
98
- target: "createIssue",
99
- data,
100
- ...data
101
- }
102
- };
103
- }
104
- function authorizationPendingResult() {
105
- return toolResult(null, [{ type: "text", text: "Authorization pending." }]);
106
- }
107
- async function annotateIssue(ctx, issue) {
108
- if (!issue?.identifier || !issue.url) {
19
+ const result = saveIssueResultSchema.safeParse(ctx.result.structuredContent);
20
+ if (!result.success) {
21
+ ctx.log.warn("linear.issue_annotation.skipped", {
22
+ "app.reason": "unexpected_save_response"
23
+ });
109
24
  return;
110
25
  }
111
- await ctx.annotations?.upsert({
26
+ const identifier = result.data.issue.identifier.toUpperCase();
27
+ await ctx.annotations.upsert({
112
28
  kind: "resource_link",
113
- key: issue.identifier.toUpperCase(),
114
- label: issue.identifier.toUpperCase(),
115
- url: issue.url,
29
+ key: identifier,
30
+ label: identifier,
31
+ url: result.data.issue.url,
116
32
  status: "open"
117
33
  });
118
34
  }
119
- function createLinearIssueTool(ctx) {
120
- return definePluginTool({
121
- description: "Create a Linear issue through Linear's hosted MCP provider and link the created issue to the current Junior conversation.",
122
- inputSchema,
123
- outputSchema,
124
- annotations: {
125
- destructiveHint: false,
126
- idempotentHint: false,
127
- openWorldHint: true,
128
- readOnlyHint: false,
129
- title: "Create Linear issue"
130
- },
131
- describeProposal(input) {
132
- return `Create a Linear issue in ${input.team}: ${input.title}`;
133
- },
134
- async execute(input, options) {
135
- const mcp = ctx.mcp;
136
- if (!mcp) {
137
- throw new Error("Linear MCP provider is unavailable.");
138
- }
139
- const result = await mcp.callTool({
140
- name: "save_issue",
141
- arguments: input,
142
- toolCallId: options.toolCallId
143
- });
144
- if (result.status === "authorization_pending") {
145
- return authorizationPendingResult();
146
- }
147
- if (result.status === "error") {
148
- throw new Error(result.message);
149
- }
150
- const issue = extractIssue(result);
151
- await annotateIssue(ctx, issue);
152
- return toolResult(issue, result.content);
153
- }
154
- });
155
- }
156
-
157
- // src/tools/update-issue.ts
158
- import {
159
- definePluginTool as definePluginTool2,
160
- pluginToolResultSchema as pluginToolResultSchema2
161
- } from "@sentry/junior-plugin-api";
162
- import { z as z2 } from "zod";
163
- var nullableName = (description) => z2.string().trim().min(1).nullable().describe(description).optional();
164
- var inputSchema2 = z2.object({
165
- id: z2.string().trim().min(1).describe("Linear issue identifier or ID to update."),
166
- assignee: nullableName(
167
- 'Assignee name, email, identifier, or "me". Use null to clear.'
168
- ),
169
- cycle: nullableName("Cycle name or identifier. Use null to clear."),
170
- description: z2.string().nullable().describe("Issue description. Use null to clear.").optional(),
171
- estimate: z2.number().nonnegative().nullable().describe("Issue estimate. Use null to clear.").optional(),
172
- labels: z2.array(z2.string().trim().min(1)).describe("Complete set of issue label names or identifiers.").optional(),
173
- priority: z2.enum(["low", "medium", "high", "urgent"]).nullable().describe("Linear priority. Use null to clear.").optional(),
174
- project: nullableName("Project name or identifier. Use null to clear."),
175
- state: z2.string().trim().min(1).describe("Workflow state name or identifier.").optional(),
176
- team: z2.string().trim().min(1).describe("Team name or identifier.").optional(),
177
- title: z2.string().trim().min(1).max(60).describe("Issue title.").optional()
178
- }).strict().superRefine((input, ctx) => {
179
- if (Object.keys(input).every((key) => key === "id")) {
180
- ctx.addIssue({
181
- code: "custom",
182
- message: "At least one issue field must be provided."
183
- });
184
- }
185
- });
186
- var outputSchema2 = pluginToolResultSchema2.extend({
187
- ok: z2.literal(true),
188
- status: z2.literal("success"),
189
- target: z2.literal("updateIssue")
190
- });
191
- function toolResult2(content) {
192
- return {
193
- content,
194
- details: {
195
- ok: true,
196
- status: "success",
197
- target: "updateIssue"
198
- }
199
- };
200
- }
201
- function authorizationPendingResult2() {
202
- return toolResult2([{ type: "text", text: "Authorization pending." }]);
203
- }
204
- function createLinearUpdateIssueTool(ctx) {
205
- return definePluginTool2({
206
- description: "Update an existing Linear issue through Linear's hosted MCP provider. Fetch the issue first when a change could overwrite existing fields.",
207
- inputSchema: inputSchema2,
208
- outputSchema: outputSchema2,
209
- annotations: {
210
- destructiveHint: false,
211
- idempotentHint: true,
212
- openWorldHint: true,
213
- readOnlyHint: false,
214
- title: "Update Linear issue"
215
- },
216
- describeProposal(input) {
217
- return `Update Linear issue ${input.id}`;
218
- },
219
- async execute(input, options) {
220
- const mcp = ctx.mcp;
221
- if (!mcp) {
222
- throw new Error("Linear MCP provider is unavailable.");
223
- }
224
- if (await mcp.prepare() === "authorization_pending") {
225
- return authorizationPendingResult2();
226
- }
227
- const result = await mcp.callTool({
228
- name: "save_issue",
229
- arguments: input,
230
- toolCallId: options.toolCallId
231
- });
232
- if (result.status === "authorization_pending") {
233
- return authorizationPendingResult2();
234
- }
235
- if (result.status === "error") {
236
- throw new Error(result.message);
237
- }
238
- return toolResult2(result.content);
239
- }
240
- });
241
- }
242
-
243
- // src/plugin.ts
244
35
  function linearPlugin() {
245
36
  return defineJuniorPlugin({
246
37
  packageName: "@sentry/junior-linear",
@@ -250,18 +41,12 @@ function linearPlugin() {
250
41
  displayName: "Linear",
251
42
  mcp: {
252
43
  transport: "http",
253
- url: "https://mcp.linear.app/mcp",
254
- wrappedTools: ["save_issue"]
44
+ url: "https://mcp.linear.app/mcp"
255
45
  },
256
46
  name: "linear"
257
47
  },
258
48
  hooks: {
259
- tools(ctx) {
260
- return {
261
- createIssue: createLinearIssueTool(ctx),
262
- updateIssue: createLinearUpdateIssueTool(ctx)
263
- };
264
- }
49
+ afterMcpTool: annotateCreatedIssue
265
50
  }
266
51
  });
267
52
  }
package/dist/plugin.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { type PluginRegistration } from "@sentry/junior-plugin-api";
2
- /** Register Linear's hosted MCP provider and Junior-owned wrapper tools. */
2
+ /** Register Linear's hosted MCP provider and conversation-link side effects. */
3
3
  export declare function linearPlugin(): PluginRegistration;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-linear",
3
- "version": "0.122.0",
3
+ "version": "0.123.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "zod": "^4.4.3",
26
- "@sentry/junior-plugin-api": "0.122.0"
26
+ "@sentry/junior-plugin-api": "0.123.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^25.9.1",
@@ -67,8 +67,7 @@ If any gate fails, revise and re-check before calling the Linear create/update t
67
67
 
68
68
  6. Execute:
69
69
 
70
- - Create new issues with `linear_createIssue`.
71
- - Update existing issues with `linear_updateIssue`.
70
+ - Create or update issues with Linear's live hosted MCP tools. Discover the current create/update tool and copy only fields justified by its live schema.
72
71
  - For updates, prefer partial changes over full rewrites. Fetch current issue state first if the mutation could overwrite structured fields or duplicate an existing comment.
73
72
  - Check for duplicates silently before creating a new issue when the request appears related to existing work.
74
73
 
@@ -21,8 +21,8 @@ The current public docs describe support for finding, creating, and updating obj
21
21
  | Intent | Minimum tool pattern |
22
22
  | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
23
23
  | Inspect an issue | Resolve the issue by key, URL, or search query, then fetch current state before answering. |
24
- | Create an issue | Confirm the team first, then create the issue with grounded title/body content and only the optional fields you can justify. |
25
- | Update fields | Fetch current issue state first, then mutate only the requested fields. |
24
+ | Create an issue | Confirm the team first, then create via the live hosted MCP tools with grounded title/body content and only fields justified by the current schema. |
25
+ | Update fields | Fetch current issue state first, then mutate via the live hosted MCP tools with only the requested fields. |
26
26
  | Add a comment | Resolve the exact issue first, then add a concise comment with durable links and next steps. |
27
27
  | Move state or assign | Read the current issue and team workflow first when state, workflow, or assignee ambiguity could cause the wrong mutation. |
28
28
  | Check for duplicates | Search for an existing matching issue before opening a new one when the request appears related to ongoing work. |
@@ -1,67 +0,0 @@
1
- import { type PluginToolResultEnvelope, type ToolRegistrationHookContext } from "@sentry/junior-plugin-api";
2
- /** Create a Linear issue through the hosted MCP provider and link it to the conversation. */
3
- export declare function createLinearIssueTool(ctx: ToolRegistrationHookContext): import("@sentry/junior-plugin-api").PluginToolDefinition<{
4
- team: string;
5
- title: string;
6
- description?: string | undefined;
7
- priority?: "low" | "medium" | "high" | "urgent" | undefined;
8
- project?: string | undefined;
9
- }, {
10
- [x: string]: unknown;
11
- ok: true;
12
- status: "success";
13
- target: "createIssue";
14
- data: {
15
- issue: {
16
- id?: string | undefined;
17
- identifier?: string | undefined;
18
- title?: string | undefined;
19
- url?: string | undefined;
20
- } | null;
21
- };
22
- issue: {
23
- id?: string | undefined;
24
- identifier?: string | undefined;
25
- title?: string | undefined;
26
- url?: string | undefined;
27
- } | null;
28
- truncated?: boolean | undefined;
29
- continuation?: {
30
- arguments: Record<string, unknown>;
31
- reason?: string | undefined;
32
- } | undefined;
33
- error?: string | {
34
- kind: string;
35
- message: string;
36
- retryable?: boolean | undefined;
37
- } | undefined;
38
- }, PluginToolResultEnvelope<{
39
- [x: string]: unknown;
40
- ok: true;
41
- status: "success";
42
- target: "createIssue";
43
- data: {
44
- issue: {
45
- id?: string | undefined;
46
- identifier?: string | undefined;
47
- title?: string | undefined;
48
- url?: string | undefined;
49
- } | null;
50
- };
51
- issue: {
52
- id?: string | undefined;
53
- identifier?: string | undefined;
54
- title?: string | undefined;
55
- url?: string | undefined;
56
- } | null;
57
- truncated?: boolean | undefined;
58
- continuation?: {
59
- arguments: Record<string, unknown>;
60
- reason?: string | undefined;
61
- } | undefined;
62
- error?: string | {
63
- kind: string;
64
- message: string;
65
- retryable?: boolean | undefined;
66
- } | undefined;
67
- }>>;
@@ -1,47 +0,0 @@
1
- import { type PluginToolResultEnvelope, type ToolRegistrationHookContext } from "@sentry/junior-plugin-api";
2
- /** Update a Linear issue through the hosted MCP provider. */
3
- export declare function createLinearUpdateIssueTool(ctx: ToolRegistrationHookContext): import("@sentry/junior-plugin-api").PluginToolDefinition<{
4
- id: string;
5
- assignee?: string | null | undefined;
6
- cycle?: string | null | undefined;
7
- description?: string | null | undefined;
8
- estimate?: number | null | undefined;
9
- labels?: string[] | undefined;
10
- priority?: "low" | "medium" | "high" | "urgent" | null | undefined;
11
- project?: string | null | undefined;
12
- state?: string | undefined;
13
- team?: string | undefined;
14
- title?: string | undefined;
15
- }, {
16
- [x: string]: unknown;
17
- ok: true;
18
- status: "success";
19
- target: "updateIssue";
20
- data?: unknown;
21
- truncated?: boolean | undefined;
22
- continuation?: {
23
- arguments: Record<string, unknown>;
24
- reason?: string | undefined;
25
- } | undefined;
26
- error?: string | {
27
- kind: string;
28
- message: string;
29
- retryable?: boolean | undefined;
30
- } | undefined;
31
- }, PluginToolResultEnvelope<{
32
- [x: string]: unknown;
33
- ok: true;
34
- status: "success";
35
- target: "updateIssue";
36
- data?: unknown;
37
- truncated?: boolean | undefined;
38
- continuation?: {
39
- arguments: Record<string, unknown>;
40
- reason?: string | undefined;
41
- } | undefined;
42
- error?: string | {
43
- kind: string;
44
- message: string;
45
- retryable?: boolean | undefined;
46
- } | undefined;
47
- }>>;