Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Backlog API とやり取りするための Model Context Protocol(MCP)サー

- プロジェクトツール(作成、読み取り、更新、削除)
- 課題とコメントの追跡(作成、更新、削除、一覧表示)
- 発生バージョン/マイルストーンの管理(作成、読み取り、更新、削除)
- Wikiページサポート
- Gitリポジトリとプルリクエストツール
- 通知ツール
Expand Down Expand Up @@ -105,7 +106,7 @@ docker pull ghcr.io/nulab/backlog-mcp-server:latest
|-----------------|--------------------------------------------------------------------------------------|
| `space` | Backlogスペース設定と一般情報を管理するためのツール |
| `project` | プロジェクト、カテゴリ、カスタムフィールド、課題タイプを管理するためのツール |
| `issue` | 課題とそのコメントを管理するためのツール |
| `issue` | 課題とそのコメント、発生バージョン/マイルストーンを管理するためのツール |
| `wiki` | Wikiページを管理するためのツール |
| `git` | Gitリポジトリとプルリクエストを管理するためのツール |
| `notifications` | ユーザー通知を管理するためのツール |
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A Model Context Protocol (MCP) server for interacting with the Backlog API. This

- Project tools (create, read, update, delete)
- Issue tracking and comments (create, update, delete, list)
- Version/Milestone management (create, read, update, delete)
- Wiki page support
- Git repository and pull request tools
- Notification tools
Expand Down Expand Up @@ -105,7 +106,7 @@ The following toolsets are available (enabled by default when `"all"` is used):
|-----------------|--------------------------------------------------------------------------------------|
| `space` | Tools for managing Backlog space settings and general information |
| `project` | Tools for managing projects, categories, custom fields, and issue types |
| `issue` | Tools for managing issues and their comments |
| `issue` | Tools for managing issues and their comments, version milestones |
| `wiki` | Tools for managing wiki pages |
| `git` | Tools for managing Git repositories and pull requests |
| `notifications` | Tools for managing user notifications |
Expand Down Expand Up @@ -184,6 +185,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.
- `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.
- `delete_version_milestone`: Deletes a version milestone.

### Toolset: `wiki`
Tools for managing wiki pages.
Expand Down
11 changes: 6 additions & 5 deletions memory-bank/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@
- ✅ Adding pull request comments (`add_pull_request_comment`)
- ✅ Updating pull request comments (`update_pull_request_comment`)

### Version/Milestone-related
- ✅ Retrieving version/milestone lists (`get_version_milestone_list`)
- ✅ Adding versions/milestones (`add_version_milestone`)
- ✅ Updating versions/milestones (`update_version_milestone`)
- ✅ Deleting versions (`delete_version`)

### Watch-related
- ✅ Retrieving watched item lists (`get_watching_list_items`)
- ✅ Retrieving watch counts (`get_watching_list_count`)
Expand Down Expand Up @@ -131,10 +137,6 @@
- ❌ Adding categories (`add_category`)
- ❌ Updating categories (`update_category`)
- ❌ Deleting categories (`delete_category`)
- ❌ Retrieving version/milestone lists (`get_version_milestone_list`)
- ❌ Adding versions/milestones (`add_version_milestone`)
- ❌ Updating versions/milestones (`update_version_milestone`)
- ❌ Deleting versions (`delete_version`)
- ❌ Retrieving custom field lists (`get_custom_field_list`)
- ❌ Adding custom fields (`add_custom_field`)
- ❌ Updating custom fields (`update_custom_field`)
Expand Down Expand Up @@ -218,7 +220,6 @@ This allows access to Backlog's main features from Claude, with optimizations fo

2. **Medium-term Goals**
- Custom field-related features
- Version/milestone-related features
- Webhook-related features
- Further performance optimizations

Expand Down
104 changes: 104 additions & 0 deletions src/tools/addVersionMilestone.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { addVersionMilestoneTool } from './addVersionMilestone.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('addVersionMilestoneTool', () => {
const mockBacklog: Partial<Backlog> = {
postVersions: jest.fn<() => Promise<any>>().mockResolvedValue({
id: 1,
projectId: 100,
name: 'Version 1.0.0',
description: 'Initial release version',
startDate: '2023-01-01T00:00:00Z',
releaseDueDate: '2023-03-31T00:00:00Z',
archived: false,
displayOrder: 1,
}),
};

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

it('returns created version milestone as formatted JSON text', async () => {
const result = await tool.handler({
projectKey: 'TEST',
name: 'Version 1.0.0',
description: 'Initial release version',
startDate: '2023-01-01T00:00:00Z',
releaseDueDate: '2023-03-31T00:00:00Z',
});

if (Array.isArray(result)) {
throw new Error('Unexpected array result');
}
expect(result.name).toEqual('Version 1.0.0');
expect(result.description).toEqual('Initial release version');
expect(result.startDate).toEqual('2023-01-01T00:00:00Z');
expect(result.releaseDueDate).toEqual('2023-03-31T00:00:00Z');
});

it('calls backlog.postVersions with correct params when using projectKey', async () => {
const params = {
projectKey: 'TEST',
name: 'Version 1.0.0',
description: 'Initial release version',
startDate: '2023-01-01T00:00:00Z',
releaseDueDate: '2024-03-31T00:00:00Z',
};

await tool.handler(params);

expect(mockBacklog.postVersions).toHaveBeenCalledWith('TEST', {
name: 'Version 1.0.0',
description: 'Initial release version',
startDate: '2023-01-01T00:00:00Z',
releaseDueDate: '2024-03-31T00:00:00Z',
});
});

it('calls backlog.postVersions with correct params when using projectId', async () => {
const params = {
projectId: 100,
name: 'Version 2.0.0',
description: 'Major release',
startDate: '2023-04-01T00:00:00Z',
releaseDueDate: '2023-06-30T00:00:00Z',
};

await tool.handler(params);

expect(mockBacklog.postVersions).toHaveBeenCalledWith(100, {
name: 'Version 2.0.0',
description: 'Major release',
startDate: '2023-04-01T00:00:00Z',
releaseDueDate: '2023-06-30T00:00:00Z',
});
});

it('calls backlog.postVersions with minimal required params', async () => {
const params = {
projectKey: 'TEST',
name: 'Quick Version',
};

await tool.handler(params);

expect(mockBacklog.postVersions).toHaveBeenCalledWith('TEST', {
name: 'Quick Version',
});
});

it('throws an error if neither projectId nor projectKey is provided', async () => {
const params = {
// projectId and projectKey are missing
name: 'Version without project',
description: 'This should fail',
};

await expect(tool.handler(params as any)).rejects.toThrow(Error);
});
});
77 changes: 77 additions & 0 deletions src/tools/addVersionMilestone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';

const addVersionMilestoneSchema = buildToolSchema((t) => ({
projectId: z
.number()
.optional()
.describe(t('TOOL_ADD_VERSION_MILESTONE_PROJECT_ID', 'Project ID')),
projectKey: z
.string()
.optional()
.describe(t('TOOL_ADD_VERSION_MILESTONE_PROJECT_KEY', 'Project key')),
name: z
.string()
.describe(t('TOOL_ADD_VERSION_MILESTONE_NAME', 'Version name')),
description: z
.string()
.optional()
.describe(
t('TOOL_ADD_VERSION_MILESTONE_DESCRIPTION', 'Version description')
),
startDate: z
.string()
.optional()
.describe(
t('TOOL_ADD_VERSION_MILESTONE_START_DATE', 'Start date of the version')
),
releaseDueDate: z
.string()
.optional()
.describe(
t(
'TOOL_ADD_VERSION_MILESTONE_RELEASE_DUE_DATE',
'Release due date of the version'
)
),
}));

export const addVersionMilestoneTool = (
backlog: Backlog,
{ t }: TranslationHelper
): ToolDefinition<
ReturnType<typeof addVersionMilestoneSchema>,
(typeof VersionSchema)['shape']
> => {
return {
name: 'add_version_milestone',
description: t(
'TOOL_ADD_VERSION_MILESTONE_DESCRIPTION',
'Creates a new version milestone'
),
schema: z.object(addVersionMilestoneSchema(t)),
outputSchema: VersionSchema,
importantFields: [
'id',
'name',
'description',
'startDate',
'releaseDueDate',
],
handler: async ({ projectId, projectKey, ...params }) => {
const result = resolveIdOrKey(
'project',
{ id: projectId, key: projectKey },
t
);
if (!result.ok) {
throw result.error;
}
return backlog.postVersions(result.value, params);
},
};
};
56 changes: 56 additions & 0 deletions src/tools/deleteVersion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { deleteVersionTool } from './deleteVersion.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('deleteVersionTool', () => {
const mockBacklog: Partial<Backlog> = {
deleteVersions: jest.fn<() => Promise<any>>().mockResolvedValue({
id: 1,
projectId: 100,
name: 'Test Version',
description: '',
startDate: null,
releaseDueDate: null,
archived: false,
displayOrder: 0,
}),
};

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

it('returns deleted version information', async () => {
const result = await tool.handler({
projectKey: 'TEST',
id: 1,
});

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

it('calls backlog.deleteVersions with correct params when using project key', async () => {
await tool.handler({
projectKey: 'TEST',
id: 1,
});

expect(mockBacklog.deleteVersions).toHaveBeenCalledWith('TEST', 1);
});

it('calls backlog.deleteVersions with correct params when using project ID', async () => {
await tool.handler({
projectId: 100,
id: 1,
});

expect(mockBacklog.deleteVersions).toHaveBeenCalledWith(100, 1);
});

it('throws an error if neither projectId nor projectKey is provided', async () => {
const params = { id: 1 }; // No identifier provided

await expect(tool.handler(params)).rejects.toThrowError(Error);
});
});
69 changes: 69 additions & 0 deletions src/tools/deleteVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';

const deleteVersionSchema = buildToolSchema((t) => ({
projectId: z
.number()
.optional()
.describe(
t(
'TOOL_DELETE_VERSION_PROJECT_ID',
'The numeric ID of the project (e.g., 12345)'
)
),
projectKey: z
.string()
.optional()
.describe(
t(
'TOOL_DELETE_VERSION_PROJECT_KEY',
"The key of the project (e.g., 'PROJECT')"
)
),
id: z
.number()
.describe(
t(
'TOOL_DELETE_VERSION_ID',
'The numeric ID of the version to delete (e.g., 67890)'
)
),
}));

export const deleteVersionTool = (
backlog: Backlog,
{ t }: TranslationHelper
): ToolDefinition<
ReturnType<typeof deleteVersionSchema>,
(typeof VersionSchema)['shape']
> => {
return {
name: 'delete_version',
description: t(
'TOOL_DELETE_VERSION_DESCRIPTION',
'Deletes a version from a project'
),
schema: z.object(deleteVersionSchema(t)),
outputSchema: VersionSchema,
handler: async ({ projectId, projectKey, id }) => {
const result = resolveIdOrKey(
'project',
{ id: projectId, key: projectKey },
t
);
if (!result.ok) {
throw result.error;
}
if (!id) {
throw new Error(
t('TOOL_DELETE_VERSION_MISSING_ID', 'Version ID is required')
);
}
return backlog.deleteVersions(result.value, id);
},
};
};
Loading