@andypai/agent-kanban 0.1.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.
Files changed (59) hide show
  1. package/README.md +89 -22
  2. package/package.json +4 -2
  3. package/src/__tests__/activity.test.ts +15 -9
  4. package/src/__tests__/api.test.ts +96 -0
  5. package/src/__tests__/board-utils.test.ts +100 -0
  6. package/src/__tests__/commands/board.test.ts +6 -13
  7. package/src/__tests__/conflict.test.ts +64 -0
  8. package/src/__tests__/index.test.ts +233 -56
  9. package/src/__tests__/jira-adf.test.ts +168 -0
  10. package/src/__tests__/jira-cache.test.ts +304 -0
  11. package/src/__tests__/jira-client.test.ts +169 -0
  12. package/src/__tests__/jira-provider-comment.test.ts +281 -0
  13. package/src/__tests__/jira-provider-mutations.test.ts +771 -0
  14. package/src/__tests__/jira-provider-read.test.ts +594 -0
  15. package/src/__tests__/jira-wiring.test.ts +187 -0
  16. package/src/__tests__/linear-cache-description-activity.test.ts +142 -0
  17. package/src/__tests__/linear-provider-comment.test.ts +243 -0
  18. package/src/__tests__/linear-provider-sync.test.ts +493 -0
  19. package/src/__tests__/local-provider-comment.test.ts +60 -0
  20. package/src/__tests__/mcp-core.test.ts +164 -0
  21. package/src/__tests__/mcp-server.test.ts +252 -0
  22. package/src/__tests__/server.test.ts +298 -0
  23. package/src/__tests__/webhooks.test.ts +604 -0
  24. package/src/activity.ts +1 -11
  25. package/src/api.ts +154 -19
  26. package/src/commands/board.ts +1 -11
  27. package/src/commands/mcp.ts +87 -0
  28. package/src/db.ts +115 -3
  29. package/src/errors.ts +2 -0
  30. package/src/id.ts +1 -1
  31. package/src/index.ts +72 -18
  32. package/src/mcp/core.ts +193 -0
  33. package/src/mcp/errors.ts +109 -0
  34. package/src/mcp/index.ts +13 -0
  35. package/src/mcp/server.ts +512 -0
  36. package/src/mcp/types.ts +72 -0
  37. package/src/providers/capabilities.ts +15 -0
  38. package/src/providers/index.ts +31 -1
  39. package/src/providers/jira-adf.ts +275 -0
  40. package/src/providers/jira-cache.ts +625 -0
  41. package/src/providers/jira-client.ts +390 -0
  42. package/src/providers/jira.ts +778 -0
  43. package/src/providers/linear-cache.ts +249 -70
  44. package/src/providers/linear-client.ts +256 -13
  45. package/src/providers/linear.ts +337 -14
  46. package/src/providers/local.ts +68 -17
  47. package/src/providers/types.ts +18 -2
  48. package/src/server.ts +139 -11
  49. package/src/tunnel.ts +79 -0
  50. package/src/types.ts +18 -2
  51. package/src/webhooks.ts +36 -0
  52. package/ui/dist/assets/index-DBnoKL_k.css +1 -0
  53. package/ui/dist/assets/index-qNVJ6clH.js +40 -0
  54. package/ui/dist/index.html +8 -2
  55. package/src/__tests__/commands/task.test.ts +0 -144
  56. package/src/commands/task.ts +0 -117
  57. package/src/fixtures.ts +0 -128
  58. package/ui/dist/assets/index-DEnUD0fq.css +0 -1
  59. package/ui/dist/assets/index-DMRjw1nI.js +0 -40
@@ -0,0 +1,512 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server'
2
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
3
+ import {
4
+ CallToolRequestSchema,
5
+ ErrorCode as JsonRpcErrorCode,
6
+ ListToolsRequestSchema,
7
+ McpError,
8
+ isInitializeRequest,
9
+ type CallToolResult,
10
+ } from '@modelcontextprotocol/sdk/types.js'
11
+ import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv'
12
+ import type {
13
+ JsonSchemaValidatorResult,
14
+ JsonSchemaType,
15
+ } from '@modelcontextprotocol/sdk/validation'
16
+ import type { TrackerCore } from './core.ts'
17
+ import { TrackerMcpError, toMcpError, toTrackerMcpError, trackerMcpJsonRpcCode } from './errors.ts'
18
+ import type { TrackerMcpAuthResolver, TrackerMcpServer, TrackerMcpTool } from './types.ts'
19
+
20
+ const EMPTY_OBJECT_SCHEMA = {
21
+ type: 'object',
22
+ properties: {},
23
+ additionalProperties: false,
24
+ } as JsonSchemaType
25
+
26
+ interface RegisteredTrackerTool<TScope> {
27
+ tool: TrackerMcpTool<TScope>
28
+ validateInput(input: unknown): JsonSchemaValidatorResult<unknown>
29
+ validateOutput?(output: unknown): JsonSchemaValidatorResult<unknown>
30
+ }
31
+
32
+ interface SessionEntry<TScope> {
33
+ server: Server
34
+ transport: WebStandardStreamableHTTPServerTransport
35
+ sessionId?: string
36
+ tools: Map<string, RegisteredTrackerTool<TScope>>
37
+ }
38
+
39
+ function isRecord(value: unknown): value is Record<string, unknown> {
40
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
41
+ }
42
+
43
+ function ticketIdFromArgs(args: unknown): string | undefined {
44
+ if (!isRecord(args) || typeof args.ticketId !== 'string') return undefined
45
+ return args.ticketId
46
+ }
47
+
48
+ function serializeToolResult(result: unknown): string {
49
+ if (typeof result === 'string') return result
50
+ try {
51
+ return JSON.stringify(result ?? null)
52
+ } catch {
53
+ return String(result)
54
+ }
55
+ }
56
+
57
+ function toCallToolResult(result: unknown): CallToolResult {
58
+ return {
59
+ content: [{ type: 'text', text: serializeToolResult(result) }],
60
+ structuredContent: { result: result ?? null },
61
+ }
62
+ }
63
+
64
+ function httpJsonRpcError(status: number, code: number, message: string): Response {
65
+ return Response.json(
66
+ {
67
+ jsonrpc: '2.0',
68
+ error: { code, message },
69
+ id: null,
70
+ },
71
+ { status },
72
+ )
73
+ }
74
+
75
+ function ticketIdSchema(extra: Record<string, JsonSchemaType> = {}): JsonSchemaType {
76
+ return {
77
+ type: 'object',
78
+ properties: { ticketId: { type: 'string' }, ...extra },
79
+ required: ['ticketId', ...Object.keys(extra)],
80
+ additionalProperties: false,
81
+ } as JsonSchemaType
82
+ }
83
+
84
+ export function defaultTools<TScope>(core: TrackerCore<TScope>): TrackerMcpTool<TScope>[] {
85
+ return [
86
+ {
87
+ name: 'getTicket',
88
+ description: 'Fetch a ticket by id.',
89
+ inputSchema: ticketIdSchema(),
90
+ handler: ({ scope, args }) =>
91
+ core.handlers.getTicket({ scope, ...(args as { ticketId: string }) }),
92
+ },
93
+ {
94
+ name: 'listComments',
95
+ description: 'List comments for a ticket.',
96
+ inputSchema: ticketIdSchema(),
97
+ handler: ({ scope, args }) =>
98
+ core.handlers.listComments({ scope, ...(args as { ticketId: string }) }),
99
+ },
100
+ {
101
+ name: 'getBoard',
102
+ description: 'Fetch the current board state.',
103
+ inputSchema: EMPTY_OBJECT_SCHEMA,
104
+ handler: ({ scope }) => core.handlers.getBoard({ scope }),
105
+ },
106
+ {
107
+ name: 'postComment',
108
+ description: 'Create a comment on a ticket.',
109
+ inputSchema: ticketIdSchema({ body: { type: 'string' } as JsonSchemaType }),
110
+ handler: ({ scope, args }) =>
111
+ core.handlers.postComment({ scope, ...(args as { ticketId: string; body: string }) }),
112
+ },
113
+ {
114
+ name: 'updateComment',
115
+ description: 'Update an existing ticket comment.',
116
+ inputSchema: ticketIdSchema({
117
+ commentId: { type: 'string' } as JsonSchemaType,
118
+ body: { type: 'string' } as JsonSchemaType,
119
+ }),
120
+ handler: ({ scope, args }) =>
121
+ core.handlers.updateComment({
122
+ scope,
123
+ ...(args as { ticketId: string; commentId: string; body: string }),
124
+ }),
125
+ },
126
+ {
127
+ name: 'moveTicket',
128
+ description: 'Move a ticket to another column.',
129
+ inputSchema: ticketIdSchema({ column: { type: 'string' } as JsonSchemaType }),
130
+ handler: ({ scope, args }) =>
131
+ core.handlers.moveTicket({
132
+ scope,
133
+ ...(args as { ticketId: string; column: string }),
134
+ }),
135
+ },
136
+ ]
137
+ }
138
+
139
+ function registerTools<TScope>(
140
+ tools: TrackerMcpTool<TScope>[],
141
+ ): Map<string, RegisteredTrackerTool<TScope>> {
142
+ const validatorProvider = new AjvJsonSchemaValidator()
143
+ const registry = new Map<string, RegisteredTrackerTool<TScope>>()
144
+
145
+ for (const tool of tools) {
146
+ if (registry.has(tool.name)) {
147
+ throw new Error(`Duplicate tracker MCP tool name '${tool.name}'`)
148
+ }
149
+ registry.set(tool.name, {
150
+ tool,
151
+ validateInput: validatorProvider.getValidator(tool.inputSchema),
152
+ validateOutput: tool.outputSchema
153
+ ? validatorProvider.getValidator(tool.outputSchema)
154
+ : undefined,
155
+ })
156
+ }
157
+
158
+ return registry
159
+ }
160
+
161
+ function isInitializePayload(body: unknown): boolean {
162
+ return Array.isArray(body)
163
+ ? body.some((message) => isInitializeRequest(message))
164
+ : isInitializeRequest(body)
165
+ }
166
+
167
+ function createSessionServer<TScope>(
168
+ core: TrackerCore<TScope>,
169
+ entry: SessionEntry<TScope>,
170
+ ): Server {
171
+ const server = new Server(
172
+ { name: 'agent-kanban-tracker-mcp', version: '1.0.0' },
173
+ { capabilities: { tools: {} } },
174
+ )
175
+
176
+ async function throwReportedToolError(input: {
177
+ scope: TScope | null
178
+ tool: string
179
+ startedAt: number
180
+ args?: unknown
181
+ error: TrackerMcpError
182
+ }): Promise<never> {
183
+ await core.notifyToolError({
184
+ scope: input.scope,
185
+ tool: input.tool,
186
+ ticketId: ticketIdFromArgs(input.args),
187
+ durationMs: Date.now() - input.startedAt,
188
+ error: input.error,
189
+ })
190
+ throw toMcpError(input.error)
191
+ }
192
+
193
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
194
+ tools: Array.from(entry.tools.values()).map(({ tool }) => ({
195
+ name: tool.name,
196
+ description: tool.description,
197
+ inputSchema: tool.inputSchema,
198
+ ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
199
+ ...(tool.annotations ? { annotations: tool.annotations } : {}),
200
+ })),
201
+ }))
202
+
203
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
204
+ const registered = entry.tools.get(request.params.name)
205
+ const rawScope = extra.authInfo?.extra?.['scope']
206
+ const rawRequest = extra.authInfo?.extra?.['request']
207
+ const scope = rawScope as TScope | null
208
+ const originalRequest = rawRequest instanceof Request ? rawRequest : null
209
+ const startedAt = Date.now()
210
+
211
+ if (!registered) {
212
+ const error = new TrackerMcpError({
213
+ code: 'validation_failed',
214
+ publicMessage: `Unknown tool '${request.params.name}'`,
215
+ })
216
+ return throwReportedToolError({
217
+ scope,
218
+ tool: request.params.name,
219
+ startedAt,
220
+ args: request.params.arguments,
221
+ error,
222
+ })
223
+ }
224
+
225
+ if (!scope || !originalRequest) {
226
+ throw new McpError(JsonRpcErrorCode.InternalError, 'Missing authenticated request context')
227
+ }
228
+
229
+ const tool = registered
230
+ const validated = tool.validateInput(request.params.arguments ?? {})
231
+ if (!validated.valid) {
232
+ const error = new TrackerMcpError({
233
+ code: 'validation_failed',
234
+ publicMessage: validated.errorMessage ?? `Invalid arguments for '${tool.tool.name}'`,
235
+ })
236
+ return throwReportedToolError({
237
+ scope,
238
+ tool: tool.tool.name,
239
+ startedAt,
240
+ args: request.params.arguments,
241
+ error,
242
+ })
243
+ }
244
+
245
+ let result: unknown
246
+ try {
247
+ result = await tool.tool.handler({
248
+ scope,
249
+ args: validated.data as Record<string, unknown>,
250
+ request: originalRequest,
251
+ })
252
+ } catch (error) {
253
+ throw toMcpError(error)
254
+ }
255
+
256
+ if (tool.validateOutput) {
257
+ const validatedOutput = tool.validateOutput(result)
258
+ if (!validatedOutput.valid) {
259
+ const error = new TrackerMcpError({
260
+ code: 'validation_failed',
261
+ publicMessage: validatedOutput.errorMessage ?? `Invalid output for '${tool.tool.name}'`,
262
+ })
263
+ return throwReportedToolError({
264
+ scope,
265
+ tool: tool.tool.name,
266
+ startedAt,
267
+ args: request.params.arguments,
268
+ error,
269
+ })
270
+ }
271
+ }
272
+
273
+ return toCallToolResult(result)
274
+ })
275
+
276
+ return server
277
+ }
278
+
279
+ export function createTrackerMcpServer<TScope>(input: {
280
+ core: TrackerCore<TScope>
281
+ auth: TrackerMcpAuthResolver<TScope>
282
+ tools?: 'default' | TrackerMcpTool<TScope>[]
283
+ }): TrackerMcpServer {
284
+ const tools =
285
+ input.tools === 'default' || input.tools === undefined ? defaultTools(input.core) : input.tools
286
+
287
+ const toolRegistry = registerTools(tools)
288
+ const sessions = new Map<string, SessionEntry<TScope>>()
289
+ const inflight = new Set<Promise<unknown>>()
290
+ let closed = false
291
+
292
+ async function trackInflight<T>(promise: Promise<T>): Promise<T> {
293
+ inflight.add(promise)
294
+ try {
295
+ return await promise
296
+ } finally {
297
+ inflight.delete(promise)
298
+ }
299
+ }
300
+
301
+ async function parsePostBody(request: Request): Promise<unknown> {
302
+ try {
303
+ return await request.json()
304
+ } catch (error) {
305
+ throw new TrackerMcpError({
306
+ code: 'validation_failed',
307
+ publicMessage: 'Invalid JSON request body',
308
+ cause: error,
309
+ })
310
+ }
311
+ }
312
+
313
+ async function authenticate(request: Request): Promise<{
314
+ scope: TScope
315
+ authInfo: {
316
+ token: string
317
+ clientId: string
318
+ scopes: string[]
319
+ extra: Record<string, unknown>
320
+ }
321
+ }> {
322
+ try {
323
+ const scope = await input.auth({
324
+ request,
325
+ url: new URL(request.url),
326
+ headers: request.headers,
327
+ })
328
+ return {
329
+ scope,
330
+ authInfo: {
331
+ token: 'tracker-mcp',
332
+ clientId: 'tracker-mcp',
333
+ scopes: [],
334
+ extra: {
335
+ scope,
336
+ request,
337
+ },
338
+ },
339
+ }
340
+ } catch (error) {
341
+ if (error instanceof TrackerMcpError && error.code === 'auth_failed') throw error
342
+ throw new TrackerMcpError({
343
+ code: 'auth_failed',
344
+ publicMessage:
345
+ error instanceof TrackerMcpError
346
+ ? (error.publicMessage ?? error.message)
347
+ : 'Authentication failed',
348
+ cause: error,
349
+ })
350
+ }
351
+ }
352
+
353
+ async function createSession(): Promise<SessionEntry<TScope>> {
354
+ const entry = {} as SessionEntry<TScope>
355
+ entry.tools = toolRegistry
356
+ entry.transport = new WebStandardStreamableHTTPServerTransport({
357
+ sessionIdGenerator: () => crypto.randomUUID(),
358
+ onsessioninitialized: (sessionId) => {
359
+ entry.sessionId = sessionId
360
+ sessions.set(sessionId, entry)
361
+ },
362
+ })
363
+ entry.server = createSessionServer(input.core, entry)
364
+ entry.transport.onclose = () => {
365
+ if (entry.sessionId) sessions.delete(entry.sessionId)
366
+ }
367
+ await entry.server.connect(entry.transport)
368
+ return entry
369
+ }
370
+
371
+ async function handleAuthenticatedRequest(request: Request): Promise<Response> {
372
+ const method = request.method.toUpperCase()
373
+ const { authInfo } = await authenticate(request)
374
+ const sessionId = request.headers.get('mcp-session-id')
375
+
376
+ if (method === 'POST') {
377
+ const parsedBody = await parsePostBody(request)
378
+
379
+ if (sessionId) {
380
+ const existing = sessions.get(sessionId)
381
+ if (!existing) {
382
+ return httpJsonRpcError(
383
+ 404,
384
+ trackerMcpJsonRpcCode('validation_failed'),
385
+ 'Session not found',
386
+ )
387
+ }
388
+ return existing.transport.handleRequest(request, { parsedBody, authInfo })
389
+ }
390
+
391
+ if (!isInitializePayload(parsedBody)) {
392
+ return httpJsonRpcError(
393
+ 400,
394
+ trackerMcpJsonRpcCode('validation_failed'),
395
+ 'Initialization required before calling tools',
396
+ )
397
+ }
398
+
399
+ const entry = await createSession()
400
+ return entry.transport.handleRequest(request, { parsedBody, authInfo })
401
+ }
402
+
403
+ if (method === 'GET' || method === 'DELETE') {
404
+ if (!sessionId) {
405
+ return httpJsonRpcError(
406
+ 400,
407
+ trackerMcpJsonRpcCode('validation_failed'),
408
+ 'Session ID header is required',
409
+ )
410
+ }
411
+ const existing = sessions.get(sessionId)
412
+ if (!existing) {
413
+ return httpJsonRpcError(
414
+ 404,
415
+ trackerMcpJsonRpcCode('validation_failed'),
416
+ 'Session not found',
417
+ )
418
+ }
419
+ return existing.transport.handleRequest(request, { authInfo })
420
+ }
421
+
422
+ return httpJsonRpcError(
423
+ 405,
424
+ JsonRpcErrorCode.InvalidRequest,
425
+ `Unsupported MCP HTTP method '${request.method}'`,
426
+ )
427
+ }
428
+
429
+ return {
430
+ async fetch(request: Request): Promise<Response> {
431
+ if (closed) {
432
+ return httpJsonRpcError(
433
+ 503,
434
+ JsonRpcErrorCode.ConnectionClosed,
435
+ 'Tracker MCP server is closed',
436
+ )
437
+ }
438
+
439
+ const responsePromise = (async () => {
440
+ const authStartedAt = Date.now()
441
+ try {
442
+ return await handleAuthenticatedRequest(request)
443
+ } catch (error) {
444
+ const trackerError = toTrackerMcpError(error)
445
+ if (trackerError.code === 'auth_failed') {
446
+ await input.core.notifyAuthFailure({
447
+ request,
448
+ durationMs: Date.now() - authStartedAt,
449
+ error: trackerError,
450
+ })
451
+ return httpJsonRpcError(
452
+ 401,
453
+ trackerMcpJsonRpcCode('auth_failed'),
454
+ trackerError.publicMessage ?? 'Unauthenticated',
455
+ )
456
+ }
457
+
458
+ if (trackerError.code === 'validation_failed') {
459
+ return httpJsonRpcError(
460
+ 400,
461
+ trackerMcpJsonRpcCode('validation_failed'),
462
+ trackerError.publicMessage ?? trackerError.message,
463
+ )
464
+ }
465
+
466
+ return httpJsonRpcError(
467
+ 500,
468
+ trackerMcpJsonRpcCode(trackerError.code),
469
+ trackerError.publicMessage ?? trackerError.message,
470
+ )
471
+ }
472
+ })()
473
+
474
+ return trackInflight(responsePromise)
475
+ },
476
+
477
+ async selfPing(): Promise<void> {
478
+ if (closed) {
479
+ throw new Error('Tracker MCP server is closed')
480
+ }
481
+ },
482
+
483
+ async close(signal?: globalThis.AbortSignal): Promise<void> {
484
+ closed = true
485
+
486
+ const closeAll = (async () => {
487
+ const entries = Array.from(sessions.values())
488
+ await Promise.allSettled(entries.map((entry) => entry.server.close()))
489
+ await Promise.allSettled(Array.from(inflight))
490
+ })()
491
+
492
+ if (!signal) {
493
+ await closeAll
494
+ return
495
+ }
496
+
497
+ const abortPromise = new Promise<never>((_, reject) => {
498
+ if (signal.aborted) {
499
+ reject(signal.reason ?? new Error('Tracker MCP close aborted'))
500
+ return
501
+ }
502
+ signal.addEventListener(
503
+ 'abort',
504
+ () => reject(signal.reason ?? new Error('Tracker MCP close aborted')),
505
+ { once: true },
506
+ )
507
+ })
508
+
509
+ await Promise.race([closeAll, abortPromise])
510
+ },
511
+ }
512
+ }
@@ -0,0 +1,72 @@
1
+ import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'
2
+ import type { JsonSchemaType } from '@modelcontextprotocol/sdk/validation'
3
+ import type { TaskComment } from '../types.ts'
4
+ import type { TrackerMcpError, TrackerMcpErrorCode } from './errors.ts'
5
+
6
+ export type TrackerMcpAuthResolver<TScope> = (ctx: {
7
+ request: Request
8
+ url: URL
9
+ headers: globalThis.Headers
10
+ }) => Promise<TScope>
11
+
12
+ export interface TrackerMcpPolicy<TScope> {
13
+ canReadTicket(scope: TScope, ticketId: string): Promise<void> | void
14
+ canPostComment(scope: TScope, ticketId: string, body: string): Promise<void> | void
15
+ canUpdateComment(
16
+ scope: TScope,
17
+ ticketId: string,
18
+ comment: TaskComment,
19
+ body: string,
20
+ ): Promise<void> | void
21
+ canMoveTicket(scope: TScope, ticketId: string, destinationColumn: string): Promise<void> | void
22
+ filterComment?(scope: TScope, comment: TaskComment): Promise<boolean> | boolean
23
+ }
24
+
25
+ export interface TrackerMcpHooks<TScope> {
26
+ onAuthFailure?(event: {
27
+ request: Request
28
+ durationMs: number
29
+ errorCode: 'auth_failed'
30
+ error: TrackerMcpError
31
+ }): Promise<void> | void
32
+
33
+ onToolStart?(event: { scope: TScope; tool: string; ticketId?: string }): Promise<void> | void
34
+
35
+ onToolResult?(event: {
36
+ scope: TScope
37
+ tool: string
38
+ ticketId?: string
39
+ durationMs: number
40
+ result?: Record<string, unknown>
41
+ }): Promise<void> | void
42
+
43
+ onToolError?(event: {
44
+ scope: TScope | null
45
+ tool: string
46
+ ticketId?: string
47
+ durationMs: number
48
+ errorCode: TrackerMcpErrorCode
49
+ error: TrackerMcpError
50
+ }): Promise<void> | void
51
+ }
52
+
53
+ export interface TrackerMcpToolHandlerContext<TScope, TArgs = Record<string, unknown>> {
54
+ scope: TScope
55
+ args: TArgs
56
+ request?: Request
57
+ }
58
+
59
+ export interface TrackerMcpTool<TScope, TArgs = Record<string, unknown>, TResult = unknown> {
60
+ name: string
61
+ description?: string
62
+ inputSchema: JsonSchemaType
63
+ annotations?: ToolAnnotations
64
+ outputSchema?: JsonSchemaType
65
+ handler(input: TrackerMcpToolHandlerContext<TScope, TArgs>): Promise<TResult> | TResult
66
+ }
67
+
68
+ export interface TrackerMcpServer {
69
+ fetch(req: Request): Promise<Response>
70
+ selfPing(): Promise<void>
71
+ close(signal?: globalThis.AbortSignal): Promise<void>
72
+ }
@@ -5,6 +5,7 @@ export const LOCAL_CAPABILITIES: ProviderCapabilities = {
5
5
  taskUpdate: true,
6
6
  taskMove: true,
7
7
  taskDelete: true,
8
+ comment: true,
8
9
  activity: true,
9
10
  metrics: true,
10
11
  columnCrud: true,
@@ -17,6 +18,20 @@ export const LINEAR_CAPABILITIES: ProviderCapabilities = {
17
18
  taskUpdate: true,
18
19
  taskMove: true,
19
20
  taskDelete: false,
21
+ comment: true,
22
+ activity: false,
23
+ metrics: false,
24
+ columnCrud: false,
25
+ bulk: false,
26
+ configEdit: false,
27
+ }
28
+
29
+ export const JIRA_CAPABILITIES: ProviderCapabilities = {
30
+ taskCreate: true,
31
+ taskUpdate: true,
32
+ taskMove: true,
33
+ taskDelete: false,
34
+ comment: true,
20
35
  activity: false,
21
36
  metrics: false,
22
37
  columnCrud: false,
@@ -1,12 +1,14 @@
1
1
  import type { Database } from 'bun:sqlite'
2
2
  import { getDbPath, initSchema, seedDefaultColumns } from '../db.ts'
3
3
  import { providerNotConfigured } from './errors.ts'
4
+ import { JiraProvider } from './jira.ts'
4
5
  import { LinearProvider } from './linear.ts'
5
6
  import { LocalProvider } from './local.ts'
6
7
  import type { KanbanProvider } from './types.ts'
7
8
 
8
9
  export function createProvider(db: Database, dbPath = getDbPath()): KanbanProvider {
9
- const providerType = (process.env['KANBAN_PROVIDER'] ?? 'local') as 'local' | 'linear'
10
+ const providerType = (process.env['KANBAN_PROVIDER'] ?? 'local') as 'local' | 'linear' | 'jira'
11
+
10
12
  if (providerType === 'linear') {
11
13
  const apiKey = process.env['LINEAR_API_KEY']
12
14
  const teamId = process.env['LINEAR_TEAM_ID']
@@ -18,6 +20,34 @@ export function createProvider(db: Database, dbPath = getDbPath()): KanbanProvid
18
20
  return new LinearProvider(db, teamId!, apiKey!)
19
21
  }
20
22
 
23
+ if (providerType === 'jira') {
24
+ const baseUrl = process.env['JIRA_BASE_URL']
25
+ const email = process.env['JIRA_EMAIL']
26
+ const apiToken = process.env['JIRA_API_TOKEN']
27
+ const projectKey = process.env['JIRA_PROJECT_KEY']
28
+ const missing: string[] = []
29
+ if (!baseUrl) missing.push('JIRA_BASE_URL')
30
+ if (!email) missing.push('JIRA_EMAIL')
31
+ if (!apiToken) missing.push('JIRA_API_TOKEN')
32
+ if (!projectKey) missing.push('JIRA_PROJECT_KEY')
33
+ if (missing.length > 0) {
34
+ providerNotConfigured(
35
+ `${missing.join(', ')} ${missing.length === 1 ? 'is' : 'are'} required when KANBAN_PROVIDER=jira`,
36
+ )
37
+ }
38
+ const boardIdRaw = process.env['JIRA_BOARD_ID']
39
+ const boardId = boardIdRaw ? Number.parseInt(boardIdRaw, 10) : undefined
40
+ const defaultIssueType = process.env['JIRA_ISSUE_TYPE'] ?? 'Task'
41
+ return new JiraProvider(db, {
42
+ baseUrl: baseUrl!,
43
+ email: email!,
44
+ apiToken: apiToken!,
45
+ projectKey: projectKey!,
46
+ boardId: Number.isFinite(boardId) ? boardId : undefined,
47
+ defaultIssueType,
48
+ })
49
+ }
50
+
21
51
  initSchema(db)
22
52
  seedDefaultColumns(db)
23
53
  return new LocalProvider(db, dbPath)