Atlassian MCP write echoes and transition lists cut to the line the model needs

hooks/register.ts 49 lines1import type { Register, ToolCallResult } from 'claude-code'
2
3const EDIT = 'mcp__atlassian__editJiraIssue'
4const COMMENT = 'mcp__atlassian__addCommentToJiraIssue'
5const TRANSITIONS = 'mcp__atlassian__getTransitionsForJiraIssue'
6
7type Transition = { id: string; name: string; to: { name: string } }
8
9// A denied or failed call keeps the server's answer: its error text is what
10// the model needs to recover.
11const landed = (out: ToolCallResult) => out.deny === undefined && !out.isError
12
13// One `id: name → status` line per transition; null when the text isn't the
14// transitions JSON, so the caller keeps the server's answer.
15function transitionLines(text: string) {
16 try {
17 const { transitions } = JSON.parse(text) as { transitions: Transition[] }
18
19 return transitions.map(t => `${t.id}: ${t.name} → ${t.to.name}`).join('\n')
20 } catch {
21 return null
22 }
23}
24
25export const register: Register = on => {
26 // The server echoes the whole issue after an edit (~1.3k tokens) and the
27 // whole comment after a comment (~1k); the model only needs to know it landed.
28 on('tool.call', { tool: EDIT }, async ($, e, next) => {
29 const out = await next(e)
30
31 return landed(out) ? { result: `Updated ${e.issueIdOrKey}: ${Object.keys(e.fields as object).join(', ')}` } : out
32 })
33
34 on('tool.call', { tool: COMMENT }, async ($, e, next) => {
35 const out = await next(e)
36
37 return landed(out) ? { result: `Commented on ${e.issueIdOrKey}` } : out
38 })
39
40 // The transitions JSON runs ~2.4k tokens, mostly icon URLs and status
41 // categories; transitionJiraIssue needs only the id.
42 on('tool.call', { tool: TRANSITIONS }, async ($, e, next) => {
43 const out = await next(e)
44 const lines = landed(out) && out.text !== undefined ? transitionLines(out.text) : null
45
46 return lines ? { result: lines } : out
47 })
48}
49