Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ Tools for managing issues, their comments, and related items like priorities, ca
- `get_resolutions`: Returns list of issue resolutions.
- `get_watching_list_items`: Returns list of watching items for a user.
- `get_watching_list_count`: Returns count of watching items for a user.
- `add_watching`: Adds a new watch to an issue.
- `update_watching`: Updates an existing watch note.
- `delete_watching`: Deletes a watch from an issue.
- `mark_watching_as_read`: Marks a watch as read.
- `get_version_milestone_list`: Returns list of version milestones for a project.
- `add_version_milestone`: Creates a new version milestone for a project.
- `update_version_milestone`: Updates an existing version milestone.
Expand Down
8 changes: 4 additions & 4 deletions memory-bank/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
### Watch-related
- ✅ Retrieving watched item lists (`get_watching_list_items`)
- ✅ Retrieving watch counts (`get_watching_list_count`)
- ✅ Adding watches (`add_watching`)
- ✅ Updating watches (`update_watching`)
- ✅ Deleting watches (`delete_watching`)
- ✅ Marking watches as read (`mark_watching_as_read`)

### Infrastructure
- ✅ MCP server implementation
Expand All @@ -78,10 +82,6 @@

### Watch-related
- ❌ Retrieving watches (`get_watching`)
- ❌ Adding watches (`add_watching`)
- ❌ Updating watches (`update_watching`)
- ❌ Deleting watches (`delete_watching`)
- ❌ Marking watches as read (`mark_watching_as_read`)

### Attachment-related
- ❌ Uploading attachments (`post_attachment_file`)
Expand Down
63 changes: 63 additions & 0 deletions src/tools/addWatching.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { addWatchingTool } from './addWatching.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('addWatchingTool', () => {
const mockBacklog: Partial<Backlog> = {
postWatchingListItem: jest.fn<() => Promise<any>>().mockResolvedValue({
id: 1,
resourceAlreadyRead: false,
note: 'Watching this issue',
type: 'issue',
issue: {
id: 1000,
projectId: 100,
issueKey: 'TEST-1',
summary: 'Test issue',
},
created: '2023-01-01T00:00:00Z',
updated: '2023-01-01T00:00:00Z',
}),
};

const mockTranslationHelper = createTranslationHelper();
const tool = addWatchingTool(mockBacklog as Backlog, mockTranslationHelper);

it('returns created watching item as formatted JSON text', async () => {
const result = await tool.handler({
issueIdOrKey: 'TEST-1',
note: 'Watching this issue',
});

if (Array.isArray(result)) {
throw new Error('Unexpected array result');
}
expect(result).toHaveProperty('id');
expect(result.note).toBe('Watching this issue');
});

it('calls backlog.postWatchingListItem with correct params when using issue key', async () => {
await tool.handler({
issueIdOrKey: 'TEST-1',
note: 'Watching this issue',
});

expect(mockBacklog.postWatchingListItem).toHaveBeenCalledWith({
issueIdOrKey: 'TEST-1',
note: 'Watching this issue',
});
});

it('calls backlog.postWatchingListItem with correct params when using issue ID', async () => {
await tool.handler({
issueIdOrKey: 1,
note: 'Important issue',
});

expect(mockBacklog.postWatchingListItem).toHaveBeenCalledWith({
issueIdOrKey: 1,
note: 'Important issue',
});
});
});
44 changes: 44 additions & 0 deletions src/tools/addWatching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { WatchingListItemSchema } from '../types/zod/backlogOutputDefinition.js';

const addWatchingSchema = buildToolSchema((t) => ({
issueIdOrKey: z
.union([z.number(), z.string()])
.describe(
t(
'TOOL_ADD_WATCHING_ISSUE_ID_OR_KEY',
'Issue ID or issue key (e.g., 1234 or "PROJECT-123")'
)
),
note: z
.string()
.describe(t('TOOL_ADD_WATCHING_NOTE', 'Optional note for the watch'))
.optional()
.default(''),
}));

export const addWatchingTool = (
backlog: Backlog,
{ t }: TranslationHelper
): ToolDefinition<
ReturnType<typeof addWatchingSchema>,
(typeof WatchingListItemSchema)['shape']
> => {
return {
name: 'add_watching',
description: t(
'TOOL_ADD_WATCHING_DESCRIPTION',
'Adds a new watch to an issue'
),
schema: z.object(addWatchingSchema(t)),
outputSchema: WatchingListItemSchema,
handler: async ({ issueIdOrKey, note }) =>
backlog.postWatchingListItem({
issueIdOrKey,
note,
}),
};
};
53 changes: 53 additions & 0 deletions src/tools/deleteWatching.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { deleteWatchingTool } from './deleteWatching.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('deleteWatchingTool', () => {
const mockBacklog: Partial<Backlog> = {
deletehWatchingListItem: jest.fn<() => Promise<any>>().mockResolvedValue({
id: 1,
resourceAlreadyRead: false,
note: 'Deleted watch',
type: 'issue',
issue: {
id: 1000,
projectId: 100,
issueKey: 'TEST-1',
summary: 'Test issue',
},
created: '2023-01-01T00:00:00Z',
updated: '2023-01-01T00:00:00Z',
}),
};

const mockTranslationHelper = createTranslationHelper();
const tool = deleteWatchingTool(
mockBacklog as Backlog,
mockTranslationHelper
);

it('deletes watching', async () => {
const result = await tool.handler({
watchId: 1,
});

expect(result).toHaveProperty('id', 1);
});

it('calls backlog.deletehWatchingListItem with correct params', async () => {
await tool.handler({
watchId: 123,
});

expect(mockBacklog.deletehWatchingListItem).toHaveBeenCalledWith(123);
});

it('handles deletion of different watch IDs', async () => {
await tool.handler({
watchId: 456,
});

expect(mockBacklog.deletehWatchingListItem).toHaveBeenCalledWith(456);
});
});
30 changes: 30 additions & 0 deletions src/tools/deleteWatching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { WatchingListItemSchema } from '../types/zod/backlogOutputDefinition.js';

const deleteWatchingSchema = buildToolSchema((t) => ({
watchId: z
.number()
.describe(t('TOOL_DELETE_WATCHING_WATCH_ID', 'Watch ID to delete')),
}));

export const deleteWatchingTool = (
backlog: Backlog,
{ t }: TranslationHelper
): ToolDefinition<
ReturnType<typeof deleteWatchingSchema>,
(typeof WatchingListItemSchema)['shape']
> => {
return {
name: 'delete_watching',
description: t(
'TOOL_DELETE_WATCHING_DESCRIPTION',
'Deletes a watch from an issue'
),
schema: z.object(deleteWatchingSchema(t)),
outputSchema: WatchingListItemSchema,
handler: async ({ watchId }) => backlog.deletehWatchingListItem(watchId),
};
};
37 changes: 37 additions & 0 deletions src/tools/markWatchingAsRead.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { markWatchingAsReadTool } from './markWatchingAsRead.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('markWatchingAsReadTool', () => {
const mockBacklog: Partial<Backlog> = {
resetWatchingListItemAsRead: jest
.fn<() => Promise<void>>()
.mockResolvedValue(undefined),
};

const mockTranslationHelper = createTranslationHelper();
const tool = markWatchingAsReadTool(
mockBacklog as Backlog,
mockTranslationHelper
);

it('returns success message as formatted JSON text', async () => {
const result = await tool.handler({
watchId: 123,
});

if (Array.isArray(result)) {
throw new Error('Unexpected array result');
}
expect(result.success).toBe(true);
});

it('calls backlog.resetWatchingListItemAsRead with correct params', async () => {
await tool.handler({
watchId: 123,
});

expect(mockBacklog.resetWatchingListItemAsRead).toHaveBeenCalledWith(123);
});
});
42 changes: 42 additions & 0 deletions src/tools/markWatchingAsRead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';

const markWatchingAsReadSchema = buildToolSchema((t) => ({
watchId: z
.number()
.describe(
t('TOOL_MARK_WATCHING_AS_READ_WATCH_ID', 'Watch ID to mark as read')
),
}));

export const MarkWatchingAsReadResultSchema = z.object({
success: z.boolean(),
message: z.string(),
});

export const markWatchingAsReadTool = (
backlog: Backlog,
{ t }: TranslationHelper
): ToolDefinition<
ReturnType<typeof markWatchingAsReadSchema>,
(typeof MarkWatchingAsReadResultSchema)['shape']
> => {
return {
name: 'mark_watching_as_read',
description: t(
'TOOL_MARK_WATCHING_AS_READ_DESCRIPTION',
'Mark a watch as read'
),
schema: z.object(markWatchingAsReadSchema(t)),
outputSchema: MarkWatchingAsReadResultSchema,
handler: async ({ watchId }) => {
await backlog.resetWatchingListItemAsRead(watchId);
return {
success: true,
message: `Watch ${watchId} marked as read`,
};
},
};
};
8 changes: 8 additions & 0 deletions src/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ import { getSpaceTool } from './getSpace.js';
import { getUsersTool } from './getUsers.js';
import { getWatchingListCountTool } from './getWatchingListCount.js';
import { getWatchingListItemsTool } from './getWatchingListItems.js';
import { addWatchingTool } from './addWatching.js';
import { updateWatchingTool } from './updateWatching.js';
import { deleteWatchingTool } from './deleteWatching.js';
import { markWatchingAsReadTool } from './markWatchingAsRead.js';
import { getWikiTool } from './getWiki.js';
import { getWikiPagesTool } from './getWikiPages.js';
import { getWikisCountTool } from './getWikisCount.js';
Expand Down Expand Up @@ -100,6 +104,10 @@ export const allTools = (
getResolutionsTool(backlog, helper),
getWatchingListItemsTool(backlog, helper),
getWatchingListCountTool(backlog, helper),
addWatchingTool(backlog, helper),
updateWatchingTool(backlog, helper),
deleteWatchingTool(backlog, helper),
markWatchingAsReadTool(backlog, helper),
getVersionMilestoneListTool(backlog, helper),
addVersionMilestoneTool(backlog, helper),
updateVersionMilestoneTool(backlog, helper),
Expand Down
55 changes: 55 additions & 0 deletions src/tools/updateWatching.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { updateWatchingTool } from './updateWatching.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('updateWatchingTool', () => {
const mockBacklog: Partial<Backlog> = {
patchWatchingListItem: jest.fn<() => Promise<any>>().mockResolvedValue({
id: 1,
resourceAlreadyRead: false,
note: 'Updated note',
type: 'issue',
issue: {
id: 1000,
projectId: 100,
issueKey: 'TEST-1',
summary: 'Test issue',
},
created: '2023-01-01T00:00:00Z',
updated: '2023-01-02T00:00:00Z',
}),
};

const mockTranslationHelper = createTranslationHelper();
const tool = updateWatchingTool(
mockBacklog as Backlog,
mockTranslationHelper
);

it('returns updated watching item', async () => {
const result = await tool.handler({
watchId: 1,
note: 'Updated note',
});

if (Array.isArray(result)) {
throw new Error('Unexpected array result');
}

expect(result).toHaveProperty('id', 1);
expect(result.note).toBe('Updated note');
});

it('calls backlog.patchWatchingListItem with correct params', async () => {
await tool.handler({
watchId: 1,
note: 'Updated note',
});

expect(mockBacklog.patchWatchingListItem).toHaveBeenCalledWith(
1,
'Updated note'
);
});
});
Loading