@andypai/agent-kanban 0.2.0 → 0.3.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 +89 -22
- package/package.json +4 -2
- package/src/__tests__/activity.test.ts +15 -9
- package/src/__tests__/api.test.ts +96 -0
- package/src/__tests__/board-utils.test.ts +100 -0
- package/src/__tests__/commands/board.test.ts +6 -13
- package/src/__tests__/conflict.test.ts +64 -0
- package/src/__tests__/index.test.ts +233 -56
- package/src/__tests__/jira-adf.test.ts +168 -0
- package/src/__tests__/jira-cache.test.ts +304 -0
- package/src/__tests__/jira-client.test.ts +169 -0
- package/src/__tests__/jira-provider-comment.test.ts +281 -0
- package/src/__tests__/jira-provider-mutations.test.ts +771 -0
- package/src/__tests__/jira-provider-read.test.ts +594 -0
- package/src/__tests__/jira-wiring.test.ts +187 -0
- package/src/__tests__/linear-cache-description-activity.test.ts +142 -0
- package/src/__tests__/linear-provider-comment.test.ts +243 -0
- package/src/__tests__/linear-provider-sync.test.ts +493 -0
- package/src/__tests__/local-provider-comment.test.ts +60 -0
- package/src/__tests__/mcp-core.test.ts +164 -0
- package/src/__tests__/mcp-server.test.ts +252 -0
- package/src/__tests__/server.test.ts +298 -0
- package/src/__tests__/webhooks.test.ts +604 -0
- package/src/activity.ts +1 -11
- package/src/api.ts +154 -19
- package/src/commands/board.ts +1 -11
- package/src/commands/mcp.ts +87 -0
- package/src/db.ts +115 -3
- package/src/errors.ts +2 -0
- package/src/id.ts +1 -1
- package/src/index.ts +72 -18
- package/src/mcp/core.ts +193 -0
- package/src/mcp/errors.ts +109 -0
- package/src/mcp/index.ts +13 -0
- package/src/mcp/server.ts +512 -0
- package/src/mcp/types.ts +72 -0
- package/src/providers/capabilities.ts +15 -0
- package/src/providers/index.ts +31 -1
- package/src/providers/jira-adf.ts +275 -0
- package/src/providers/jira-cache.ts +625 -0
- package/src/providers/jira-client.ts +390 -0
- package/src/providers/jira.ts +778 -0
- package/src/providers/linear-cache.ts +249 -70
- package/src/providers/linear-client.ts +256 -13
- package/src/providers/linear.ts +337 -14
- package/src/providers/local.ts +68 -17
- package/src/providers/types.ts +18 -2
- package/src/server.ts +139 -11
- package/src/tunnel.ts +79 -0
- package/src/types.ts +18 -2
- package/src/webhooks.ts +36 -0
- package/ui/dist/assets/index-DBnoKL_k.css +1 -0
- package/ui/dist/assets/index-qNVJ6clH.js +40 -0
- package/ui/dist/index.html +2 -2
- package/src/__tests__/commands/task.test.ts +0 -144
- package/src/commands/task.ts +0 -117
- package/src/fixtures.ts +0 -128
- package/ui/dist/assets/index-B8f9NB4z.css +0 -1
- package/ui/dist/assets/index-zWp-rB7b.js +0 -40
package/src/mcp/core.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { KanbanProvider } from '../providers/types.ts'
|
|
2
|
+
import type { Task, TaskComment } from '../types.ts'
|
|
3
|
+
import { TrackerMcpError, toTrackerMcpError } from './errors.ts'
|
|
4
|
+
import type { TrackerMcpHooks, TrackerMcpPolicy } from './types.ts'
|
|
5
|
+
|
|
6
|
+
export interface TrackerCore<TScope> {
|
|
7
|
+
notifyAuthFailure(input: {
|
|
8
|
+
request: Request
|
|
9
|
+
durationMs: number
|
|
10
|
+
error: TrackerMcpError
|
|
11
|
+
}): Promise<void>
|
|
12
|
+
notifyToolError(input: {
|
|
13
|
+
scope: TScope | null
|
|
14
|
+
tool: string
|
|
15
|
+
ticketId?: string
|
|
16
|
+
durationMs: number
|
|
17
|
+
error: TrackerMcpError
|
|
18
|
+
}): Promise<void>
|
|
19
|
+
handlers: {
|
|
20
|
+
getTicket(input: { scope: TScope; ticketId: string }): Promise<Task>
|
|
21
|
+
listComments(input: { scope: TScope; ticketId: string }): Promise<TaskComment[]>
|
|
22
|
+
getBoard(input: { scope: TScope }): Promise<Awaited<ReturnType<KanbanProvider['getBoard']>>>
|
|
23
|
+
postComment(input: { scope: TScope; ticketId: string; body: string }): Promise<TaskComment>
|
|
24
|
+
updateComment(input: {
|
|
25
|
+
scope: TScope
|
|
26
|
+
ticketId: string
|
|
27
|
+
commentId: string
|
|
28
|
+
body: string
|
|
29
|
+
}): Promise<TaskComment>
|
|
30
|
+
moveTicket(input: { scope: TScope; ticketId: string; column: string }): Promise<void>
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface RunToolInput<TScope, TResult> {
|
|
35
|
+
scope: TScope
|
|
36
|
+
tool: string
|
|
37
|
+
ticketId?: string
|
|
38
|
+
execute(): Promise<TResult>
|
|
39
|
+
resultMeta?: Record<string, unknown> | ((result: TResult) => Record<string, unknown> | undefined)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function filterComments<TScope>(
|
|
43
|
+
scope: TScope,
|
|
44
|
+
comments: TaskComment[],
|
|
45
|
+
policy: TrackerMcpPolicy<TScope>,
|
|
46
|
+
): Promise<TaskComment[]> {
|
|
47
|
+
if (!policy.filterComment) return comments
|
|
48
|
+
const allowed = await Promise.all(
|
|
49
|
+
comments.map((comment) => policy.filterComment!(scope, comment)),
|
|
50
|
+
)
|
|
51
|
+
return comments.filter((_, index) => allowed[index])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createTrackerCore<TScope>(input: {
|
|
55
|
+
provider: KanbanProvider
|
|
56
|
+
policy: TrackerMcpPolicy<TScope>
|
|
57
|
+
hooks?: TrackerMcpHooks<TScope>
|
|
58
|
+
}): TrackerCore<TScope> {
|
|
59
|
+
const { provider, policy } = input
|
|
60
|
+
const hooks = input.hooks ?? {}
|
|
61
|
+
|
|
62
|
+
async function runTool<TResult>({
|
|
63
|
+
scope,
|
|
64
|
+
tool,
|
|
65
|
+
ticketId,
|
|
66
|
+
execute,
|
|
67
|
+
resultMeta,
|
|
68
|
+
}: RunToolInput<TScope, TResult>): Promise<TResult> {
|
|
69
|
+
const startedAt = Date.now()
|
|
70
|
+
await hooks.onToolStart?.({ scope, tool, ticketId })
|
|
71
|
+
try {
|
|
72
|
+
const result = await execute()
|
|
73
|
+
const hookResult = typeof resultMeta === 'function' ? resultMeta(result) : resultMeta
|
|
74
|
+
await hooks.onToolResult?.({
|
|
75
|
+
scope,
|
|
76
|
+
tool,
|
|
77
|
+
ticketId,
|
|
78
|
+
durationMs: Date.now() - startedAt,
|
|
79
|
+
result: hookResult,
|
|
80
|
+
})
|
|
81
|
+
return result
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const trackerError = toTrackerMcpError(error)
|
|
84
|
+
await hooks.onToolError?.({
|
|
85
|
+
scope,
|
|
86
|
+
tool,
|
|
87
|
+
ticketId,
|
|
88
|
+
durationMs: Date.now() - startedAt,
|
|
89
|
+
errorCode: trackerError.code,
|
|
90
|
+
error: trackerError,
|
|
91
|
+
})
|
|
92
|
+
throw trackerError
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
async notifyAuthFailure({ request, durationMs, error }) {
|
|
98
|
+
await hooks.onAuthFailure?.({
|
|
99
|
+
request,
|
|
100
|
+
durationMs,
|
|
101
|
+
errorCode: 'auth_failed',
|
|
102
|
+
error,
|
|
103
|
+
})
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async notifyToolError({ scope, tool, ticketId, durationMs, error }) {
|
|
107
|
+
await hooks.onToolError?.({
|
|
108
|
+
scope,
|
|
109
|
+
tool,
|
|
110
|
+
ticketId,
|
|
111
|
+
durationMs,
|
|
112
|
+
errorCode: error.code,
|
|
113
|
+
error,
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
handlers: {
|
|
118
|
+
getTicket({ scope, ticketId }) {
|
|
119
|
+
return runTool({
|
|
120
|
+
scope,
|
|
121
|
+
tool: 'getTicket',
|
|
122
|
+
ticketId,
|
|
123
|
+
execute: async () => {
|
|
124
|
+
await policy.canReadTicket(scope, ticketId)
|
|
125
|
+
return provider.getTask(ticketId)
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
listComments({ scope, ticketId }) {
|
|
131
|
+
return runTool({
|
|
132
|
+
scope,
|
|
133
|
+
tool: 'listComments',
|
|
134
|
+
ticketId,
|
|
135
|
+
execute: async () => {
|
|
136
|
+
await policy.canReadTicket(scope, ticketId)
|
|
137
|
+
const comments = await provider.listComments(ticketId)
|
|
138
|
+
return filterComments(scope, comments, policy)
|
|
139
|
+
},
|
|
140
|
+
resultMeta: (comments) => ({ commentCount: comments.length }),
|
|
141
|
+
})
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
getBoard({ scope }) {
|
|
145
|
+
return runTool({
|
|
146
|
+
scope,
|
|
147
|
+
tool: 'getBoard',
|
|
148
|
+
execute: async () => provider.getBoard(),
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
postComment({ scope, ticketId, body }) {
|
|
153
|
+
return runTool({
|
|
154
|
+
scope,
|
|
155
|
+
tool: 'postComment',
|
|
156
|
+
ticketId,
|
|
157
|
+
execute: async () => {
|
|
158
|
+
await policy.canPostComment(scope, ticketId, body)
|
|
159
|
+
return provider.comment(ticketId, body)
|
|
160
|
+
},
|
|
161
|
+
resultMeta: (comment) => ({ commentId: comment.id }),
|
|
162
|
+
})
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
updateComment({ scope, ticketId, commentId, body }) {
|
|
166
|
+
return runTool({
|
|
167
|
+
scope,
|
|
168
|
+
tool: 'updateComment',
|
|
169
|
+
ticketId,
|
|
170
|
+
execute: async () => {
|
|
171
|
+
const existing = await provider.getComment(ticketId, commentId)
|
|
172
|
+
await policy.canUpdateComment(scope, ticketId, existing, body)
|
|
173
|
+
return provider.updateComment(ticketId, commentId, body)
|
|
174
|
+
},
|
|
175
|
+
resultMeta: (comment) => ({ commentId: comment.id }),
|
|
176
|
+
})
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
moveTicket({ scope, ticketId, column }) {
|
|
180
|
+
return runTool({
|
|
181
|
+
scope,
|
|
182
|
+
tool: 'moveTicket',
|
|
183
|
+
ticketId,
|
|
184
|
+
execute: async () => {
|
|
185
|
+
await policy.canMoveTicket(scope, ticketId, column)
|
|
186
|
+
await provider.moveTask(ticketId, column)
|
|
187
|
+
},
|
|
188
|
+
resultMeta: { movedTo: column },
|
|
189
|
+
})
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { McpError, ErrorCode as JsonRpcErrorCode } from '@modelcontextprotocol/sdk/types.js'
|
|
2
|
+
import { ErrorCode, type ErrorCodeValue, KanbanError } from '../errors.ts'
|
|
3
|
+
|
|
4
|
+
export type TrackerMcpErrorCode =
|
|
5
|
+
| 'auth_failed'
|
|
6
|
+
| 'policy_denied'
|
|
7
|
+
| 'ticket_not_found'
|
|
8
|
+
| 'comment_not_found'
|
|
9
|
+
| 'validation_failed'
|
|
10
|
+
| 'provider_unavailable'
|
|
11
|
+
| 'internal_error'
|
|
12
|
+
|
|
13
|
+
export class TrackerMcpError extends Error {
|
|
14
|
+
override readonly name = 'TrackerMcpError'
|
|
15
|
+
readonly code: TrackerMcpErrorCode
|
|
16
|
+
override readonly cause?: unknown
|
|
17
|
+
readonly publicMessage?: string
|
|
18
|
+
|
|
19
|
+
constructor(input: {
|
|
20
|
+
code: TrackerMcpErrorCode
|
|
21
|
+
message?: string
|
|
22
|
+
publicMessage?: string
|
|
23
|
+
cause?: unknown
|
|
24
|
+
}) {
|
|
25
|
+
super(input.message ?? input.publicMessage ?? input.code)
|
|
26
|
+
this.code = input.code
|
|
27
|
+
this.cause = input.cause
|
|
28
|
+
this.publicMessage = input.publicMessage
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function providerError(code: ErrorCodeValue): TrackerMcpErrorCode {
|
|
33
|
+
switch (code) {
|
|
34
|
+
case ErrorCode.TASK_NOT_FOUND:
|
|
35
|
+
return 'ticket_not_found'
|
|
36
|
+
case ErrorCode.COMMENT_NOT_FOUND:
|
|
37
|
+
return 'comment_not_found'
|
|
38
|
+
case ErrorCode.COLUMN_NOT_FOUND:
|
|
39
|
+
case ErrorCode.INVALID_METADATA:
|
|
40
|
+
case ErrorCode.INVALID_POSITION:
|
|
41
|
+
case ErrorCode.INVALID_PRIORITY:
|
|
42
|
+
case ErrorCode.MISSING_ARGUMENT:
|
|
43
|
+
case ErrorCode.UNSUPPORTED_OPERATION:
|
|
44
|
+
case ErrorCode.CONFLICT:
|
|
45
|
+
return 'validation_failed'
|
|
46
|
+
case ErrorCode.PROVIDER_AUTH_FAILED:
|
|
47
|
+
case ErrorCode.PROVIDER_RATE_LIMITED:
|
|
48
|
+
case ErrorCode.PROVIDER_UPSTREAM_ERROR:
|
|
49
|
+
case ErrorCode.PROVIDER_SYNC_REQUIRED:
|
|
50
|
+
case ErrorCode.PROVIDER_NOT_CONFIGURED:
|
|
51
|
+
return 'provider_unavailable'
|
|
52
|
+
default:
|
|
53
|
+
return 'internal_error'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function toTrackerMcpError(error: unknown): TrackerMcpError {
|
|
58
|
+
if (error instanceof TrackerMcpError) return error
|
|
59
|
+
if (error instanceof KanbanError) {
|
|
60
|
+
return new TrackerMcpError({
|
|
61
|
+
code: providerError(error.code),
|
|
62
|
+
message: error.message,
|
|
63
|
+
publicMessage: error.message,
|
|
64
|
+
cause: error,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
if (error instanceof Error) {
|
|
68
|
+
return new TrackerMcpError({
|
|
69
|
+
code: 'internal_error',
|
|
70
|
+
message: error.message,
|
|
71
|
+
publicMessage: error.message,
|
|
72
|
+
cause: error,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
return new TrackerMcpError({
|
|
76
|
+
code: 'internal_error',
|
|
77
|
+
message: String(error),
|
|
78
|
+
publicMessage: String(error),
|
|
79
|
+
cause: error,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function trackerMcpJsonRpcCode(code: TrackerMcpErrorCode): number {
|
|
84
|
+
switch (code) {
|
|
85
|
+
case 'auth_failed':
|
|
86
|
+
return -32001
|
|
87
|
+
case 'policy_denied':
|
|
88
|
+
return -32002
|
|
89
|
+
case 'ticket_not_found':
|
|
90
|
+
case 'comment_not_found':
|
|
91
|
+
return -32003
|
|
92
|
+
case 'validation_failed':
|
|
93
|
+
return JsonRpcErrorCode.InvalidParams
|
|
94
|
+
case 'provider_unavailable':
|
|
95
|
+
return -32010
|
|
96
|
+
case 'internal_error':
|
|
97
|
+
default:
|
|
98
|
+
return JsonRpcErrorCode.InternalError
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function toMcpError(error: unknown): McpError {
|
|
103
|
+
const trackerError = toTrackerMcpError(error)
|
|
104
|
+
return new McpError(
|
|
105
|
+
trackerMcpJsonRpcCode(trackerError.code),
|
|
106
|
+
trackerError.publicMessage ?? trackerError.message,
|
|
107
|
+
{ trackerMcpCode: trackerError.code },
|
|
108
|
+
)
|
|
109
|
+
}
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { createTrackerCore } from './core.ts'
|
|
2
|
+
export { createTrackerMcpServer } from './server.ts'
|
|
3
|
+
export { TrackerMcpError, type TrackerMcpErrorCode } from './errors.ts'
|
|
4
|
+
export type { TrackerCore } from './core.ts'
|
|
5
|
+
export type {
|
|
6
|
+
TrackerMcpAuthResolver,
|
|
7
|
+
TrackerMcpHooks,
|
|
8
|
+
TrackerMcpPolicy,
|
|
9
|
+
TrackerMcpServer,
|
|
10
|
+
TrackerMcpTool,
|
|
11
|
+
TrackerMcpToolHandlerContext,
|
|
12
|
+
} from './types.ts'
|
|
13
|
+
export { defaultTools } from './server.ts'
|