#
tokens: 49557/50000 102/164 files (page 1/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 3. Use http://codebase.md/nulab/backlog-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .clineigonre
├── .clinerules
│   └── commit-conventional-format.md
├── .env.example
├── .github
│   └── workflows
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .release-it.json
├── .tool-versions
├── CHANGELOG.md
├── Dockerfile
├── eslint.config.js
├── jest.config.js
├── LICENSE
├── memory-bank
│   ├── activeContext.md
│   ├── productContext.md
│   ├── progress.md
│   ├── projectbrief.md
│   ├── systemPatterns.md
│   ├── techContext.md
│   └── URLlist.md
├── package-lock.json
├── package.json
├── README.ja.md
├── README.md
├── scripts
│   └── replace-version.js
├── src
│   ├── backlog
│   │   ├── backlogErrorHandler.ts
│   │   ├── customFields.test.ts
│   │   ├── customFields.ts
│   │   └── parseBacklogAPIError.ts
│   ├── createTranslationHelper.test.ts
│   ├── createTranslationHelper.ts
│   ├── handlers
│   │   ├── builders
│   │   │   ├── composeToolHandler.test.ts
│   │   │   └── composeToolHandler.ts
│   │   └── transformers
│   │       ├── wrapWithErrorHandling.test.ts
│   │       ├── wrapWithErrorHandling.ts
│   │       ├── wrapWithFieldPicking.test.ts
│   │       ├── wrapWithFieldPicking.ts
│   │       ├── wrapWithTokenLimit.test.ts
│   │       ├── wrapWithTokenLimit.ts
│   │       ├── wrapWithToolResult.test.ts
│   │       └── wrapWithToolResult.ts
│   ├── index.ts
│   ├── registerTools.test.ts
│   ├── registerTools.ts
│   ├── tools
│   │   ├── addIssue.test.ts
│   │   ├── addIssue.ts
│   │   ├── addIssueComment.test.ts
│   │   ├── addIssueComment.ts
│   │   ├── addProject.test.ts
│   │   ├── addProject.ts
│   │   ├── addPullRequest.test.ts
│   │   ├── addPullRequest.ts
│   │   ├── addPullRequestComment.test.ts
│   │   ├── addPullRequestComment.ts
│   │   ├── addVersionMilestone.test.ts
│   │   ├── addVersionMilestone.ts
│   │   ├── addWiki.test.ts
│   │   ├── addWiki.ts
│   │   ├── countIssues.test.ts
│   │   ├── countIssues.ts
│   │   ├── deleteIssue.test.ts
│   │   ├── deleteIssue.ts
│   │   ├── deleteProject.test.ts
│   │   ├── deleteProject.ts
│   │   ├── deleteVersion.test.ts
│   │   ├── deleteVersion.ts
│   │   ├── dynamicTools
│   │   │   ├── toolsets.test.ts
│   │   │   └── toolsets.ts
│   │   ├── getCategories.test.ts
│   │   ├── getCategories.ts
│   │   ├── getCustomFields.test.ts
│   │   ├── getCustomFields.ts
│   │   ├── getDocument.test.ts
│   │   ├── getDocument.ts
│   │   ├── getDocuments.test.ts
│   │   ├── getDocuments.ts
│   │   ├── getDocumentTree.test.ts
│   │   ├── getDocumentTree.ts
│   │   ├── getGitRepositories.test.ts
│   │   ├── getGitRepositories.ts
│   │   ├── getGitRepository.test.ts
│   │   ├── getGitRepository.ts
│   │   ├── getIssue.test.ts
│   │   ├── getIssue.ts
│   │   ├── getIssueComments.test.ts
│   │   ├── getIssueComments.ts
│   │   ├── getIssues.test.ts
│   │   ├── getIssues.ts
│   │   ├── getIssueTypes.test.ts
│   │   ├── getIssueTypes.ts
│   │   ├── getMyself.test.ts
│   │   ├── getMyself.ts
│   │   ├── getNotifications.test.ts
│   │   ├── getNotifications.ts
│   │   ├── getNotificationsCount.test.ts
│   │   ├── getNotificationsCount.ts
│   │   ├── getPriorities.test.ts
│   │   ├── getPriorities.ts
│   │   ├── getProject.test.ts
│   │   ├── getProject.ts
│   │   ├── getProjectList.test.ts
│   │   ├── getProjectList.ts
│   │   ├── getPullRequest.test.ts
│   │   ├── getPullRequest.ts
│   │   ├── getPullRequestComments.test.ts
│   │   ├── getPullRequestComments.ts
│   │   ├── getPullRequests.test.ts
│   │   ├── getPullRequests.ts
│   │   ├── getPullRequestsCount.test.ts
│   │   ├── getPullRequestsCount.ts
│   │   ├── getResolutions.test.ts
│   │   ├── getResolutions.ts
│   │   ├── getSpace.test.ts
│   │   ├── getSpace.ts
│   │   ├── getUsers.test.ts
│   │   ├── getUsers.ts
│   │   ├── getVersionMilestoneList.test.ts
│   │   ├── getVersionMilestoneList.ts
│   │   ├── getWatchingListCount.test.ts
│   │   ├── getWatchingListCount.ts
│   │   ├── getWatchingListItems.test.ts
│   │   ├── getWatchingListItems.ts
│   │   ├── getWiki.test.ts
│   │   ├── getWiki.ts
│   │   ├── getWikiPages.test.ts
│   │   ├── getWikiPages.ts
│   │   ├── getWikisCount.test.ts
│   │   ├── getWikisCount.ts
│   │   ├── markNotificationAsRead.test.ts
│   │   ├── markNotificationAsRead.ts
│   │   ├── resetUnreadNotificationCount.test.ts
│   │   ├── resetUnreadNotificationCount.ts
│   │   ├── tools.ts
│   │   ├── updateIssue.test.ts
│   │   ├── updateIssue.ts
│   │   ├── updateProject.test.ts
│   │   ├── updateProject.ts
│   │   ├── updatePullRequest.test.ts
│   │   ├── updatePullRequest.ts
│   │   ├── updatePullRequestComment.test.ts
│   │   ├── updatePullRequestComment.ts
│   │   ├── updateVersionMilestone.test.ts
│   │   └── updateVersionMilestone.ts
│   ├── types
│   │   ├── mcp.ts
│   │   ├── result.ts
│   │   ├── tool.ts
│   │   ├── toolsets.ts
│   │   └── zod
│   │       └── backlogOutputDefinition.ts
│   ├── utils
│   │   ├── generateFieldsDescription.test.ts
│   │   ├── generateFieldsDescription.ts
│   │   ├── logger.ts
│   │   ├── resolveIdOrKey.test.ts
│   │   ├── resolveIdOrKey.ts
│   │   ├── runToolSafely.test.ts
│   │   ├── runToolSafely.ts
│   │   ├── tokenCounter.test.ts
│   │   ├── tokenCounter.ts
│   │   ├── toolRegistrar.test.ts
│   │   ├── toolRegistrar.ts
│   │   ├── toolsetUtils.test.ts
│   │   ├── toolsetUtils.ts
│   │   ├── wrapServerWithToolRegistry.test.ts
│   │   └── wrapServerWithToolRegistry.ts
│   └── version.template.ts
├── translationConfig
│   └── .backlog-mcp-serverrc.json.example
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------

```
1 | nodejs 22.0.0
2 | 
```

--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------

```
1 | build
2 | node_modules
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
1 | BACKLOG_API_KEY=
2 | BACKLOG_DOMAIN=
3 | 
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
1 | {
2 |   "singleQuote": true,
3 |   "semi": true,
4 |   "trailingComma": "es5"
5 | }
6 | 
```

--------------------------------------------------------------------------------
/.clineigonre:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | **/node_modules/
 4 | .pnp
 5 | .pnp.js
 6 | 
 7 | # Build outputs
 8 | /build/
 9 | /dist/
10 | /.next/
11 | /out/
12 | 
13 | # Testing
14 | /coverage/
15 | 
16 | # Environment variables
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 | 
23 | # Large data files
24 | *.csv
25 | *.xlsx
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | npm-debug.log*
 4 | yarn-debug.log*
 5 | yarn-error.log*
 6 | package-lock.json
 7 | yarn.lock
 8 | 
 9 | # TypeScript build output
10 | build/
11 | dist/
12 | *.tsbuildinfo
13 | 
14 | # Environment variables
15 | .env
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | 
21 | # Editor directories and files
22 | .idea/
23 | .vscode/*
24 | !.vscode/extensions.json
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | *.suo
29 | *.ntvs*
30 | *.njsproj
31 | *.sln
32 | *.sw?
33 | 
34 | # OS specific
35 | .DS_Store
36 | Thumbs.db
37 | Desktop.ini
38 | 
39 | # Logs
40 | logs/
41 | *.log
42 | npm-debug.log*
43 | yarn-debug.log*
44 | yarn-error.log*
45 | 
46 | # Testing
47 | coverage/
48 | .nyc_output/
49 | 
50 | # Temporary files
51 | tmp/
52 | temp/
53 | 
54 | src/version.ts
```

--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "git": {
 3 |     "tagName": "v${version}",
 4 |     "commitMessage": "chore(bump): v${version}",
 5 |     "requireCleanWorkingDir": true
 6 |   },
 7 |   "plugins": {
 8 |     "@release-it/conventional-changelog": {
 9 |       "preset": "conventionalcommits",
10 |       "infile": "CHANGELOG.md",
11 |       "changelogHeader": "# Changelog"
12 |     }
13 |   },
14 |   "github": {
15 |     "release": true,
16 |     "releaseName": "v${version}",
17 |     "tokenRef": "GITHUB_TOKEN"
18 |   },
19 |   "npm": false,
20 |   "bumpFiles": ["package.json"],
21 |   "hooks": {
22 |     "after:bump": "docker buildx build --platform linux/amd64,linux/arm64 --provenance=false --sbom=false --build-arg VERSION=${version} -t ghcr.io/nulab/backlog-mcp-server:v${version} -t ghcr.io/nulab/backlog-mcp-server:latest --push ."
23 |   }
24 | }
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Backlog MCP Server
  2 | 
  3 | ![MIT License](https://img.shields.io/badge/license-MIT-green.svg)
  4 | ![Build](https://github.com/nulab/backlog-mcp-server/actions/workflows/ci.yml/badge.svg)
  5 | ![Last Commit](https://img.shields.io/github/last-commit/nulab/backlog-mcp-server.svg)
  6 | 
  7 | [📘 日本語でのご利用ガイド](./README.ja.md) 
  8 | 
  9 | A Model Context Protocol (MCP) server for interacting with the Backlog API. This server provides tools for managing projects, issues, wiki pages, and more in Backlog through AI agents like Claude Desktop / Cline / Cursor etc.
 10 | 
 11 | ## Features
 12 | 
 13 | - Project tools (create, read, update, delete)
 14 | - Issue tracking and comments (create, update, delete, list)
 15 | - Version/Milestone management (create, read, update, delete)
 16 | - Wiki page support
 17 | - Git repository and pull request tools
 18 | - Notification tools
 19 | - GraphQL-style field selection for optimized responses
 20 | - Token limiting for large responses
 21 | 
 22 | ## Getting Started
 23 | 
 24 | ### Requirements
 25 | 
 26 | - Docker
 27 | - A Backlog account with API access
 28 | - API key from your Backlog account
 29 | 
 30 | ### Option 1: Install via Docker
 31 | 
 32 | The easiest way to use this MCP server is through MCP configurations:
 33 | 
 34 | 1. Open MCP settings
 35 | 2. Navigate to the MCP configuration section
 36 | 3. Add the following configuration:
 37 | 
 38 | ```json
 39 | {
 40 |   "mcpServers": {
 41 |     "backlog": {
 42 |       "command": "docker",
 43 |       "args": [
 44 |         "run",
 45 |         "--pull", "always",
 46 |         "-i",
 47 |         "--rm",
 48 |         "-e", "BACKLOG_DOMAIN",
 49 |         "-e", "BACKLOG_API_KEY",
 50 |         "ghcr.io/nulab/backlog-mcp-server"
 51 |       ],
 52 |       "env": {
 53 |         "BACKLOG_DOMAIN": "your-domain.backlog.com",
 54 |         "BACKLOG_API_KEY": "your-api-key"
 55 |       }
 56 |     }
 57 |   }
 58 | }
 59 | ```
 60 | 
 61 | Replace `your-domain.backlog.com` with your Backlog domain and `your-api-key` with your Backlog API key.
 62 | 
 63 | ✅ If you cannot use --pull always, you can manually update the image using:
 64 | 
 65 | ```
 66 | docker pull ghcr.io/nulab/backlog-mcp-server:latest
 67 | ```
 68 | 
 69 | ### Option 2: Install via npx
 70 | 
 71 | You can also run the server directly using `npx` without cloning the repository. This is a convenient way to run the server without a full installation.
 72 | 
 73 | 1. Open MCP settings
 74 | 2. Navigate to the MCP configuration section
 75 | 3. Add the following configuration:
 76 | 
 77 | ```json
 78 | {
 79 |   "mcpServers": {
 80 |     "backlog": {
 81 |       "command": "npx",
 82 |       "args": [
 83 |         "backlog-mcp-server"
 84 |       ],
 85 |       "env": {
 86 |         "BACKLOG_DOMAIN": "your-domain.backlog.com",
 87 |         "BACKLOG_API_KEY": "your-api-key"
 88 |       }
 89 |     }
 90 |   }
 91 | }
 92 | ```
 93 | 
 94 | Replace `your-domain.backlog.com` with your Backlog domain and `your-api-key` with your Backlog API key.
 95 | 
 96 | ### Option 3: Manual Setup (Node.js)
 97 | 
 98 | 1. Clone and install:
 99 |    ```bash
100 |    git clone https://github.com/nulab/backlog-mcp-server.git
101 |    cd backlog-mcp-server
102 |    npm install
103 |    npm run build
104 |    ```
105 | 
106 | 2. Set your json to use as MCP
107 |   ```json
108 |   {
109 |     "mcpServers": {
110 |       "backlog": {
111 |         "command": "node",
112 |         "args": [
113 |           "your-repository-location/build/index.js"
114 |         ],
115 |         "env": {
116 |           "BACKLOG_DOMAIN": "your-domain.backlog.com",
117 |           "BACKLOG_API_KEY": "your-api-key"
118 |         }
119 |       }
120 |     }
121 |   }
122 |   ```
123 | 
124 | ## Tool Configuration
125 | 
126 | You can selectively enable or disable specific **toolsets** using the `--enable-toolsets` command-line flag or the `ENABLE_TOOLSETS` environment variable. This allows better control over which tools are available to the AI agent and helps reduce context size.
127 | 
128 | ### Available Toolsets
129 | 
130 | The following toolsets are available (enabled by default when `"all"` is used):
131 | 
132 | | Toolset         | Description                                                                          |
133 | |-----------------|--------------------------------------------------------------------------------------|
134 | | `space`         | Tools for managing Backlog space settings and general information                   |
135 | | `project`       | Tools for managing projects, categories, custom fields, and issue types              |
136 | | `issue`         | Tools for managing issues and their comments, version milestones                    |
137 | | `wiki`          | Tools for managing wiki pages                                                        |
138 | | `git`           | Tools for managing Git repositories and pull requests                                |
139 | | `notifications` | Tools for managing user notifications                                                |
140 | | `document`      | Tools for viewing documents and document trees                   |
141 | 
142 | ### Specifying Toolsets
143 | 
144 | You can control toolset activation in the following ways:
145 | 
146 | Using via CLI:
147 | 
148 | ```bash
149 | --enable-toolsets space,project,issue
150 | ```
151 | 
152 | Or via environment variable:
153 | 
154 | ```
155 | ENABLE_TOOLSETS="space,project,issue"
156 | ```
157 | 
158 | If all is specified, all available toolsets will be enabled. This is also the default behavior.
159 | 
160 | Using selective toolsets can be helpful if the toolset list is too large for your AI agent or if certain tools are causing performance issues. In such cases, disabling unused toolsets may improve stability.
161 | 
162 | > 🧩 Tip: `project` toolset is highly recommended, as many other tools rely on project data as an entry point.
163 | 
164 | ### Dynamic Toolset Discovery (Experimental)
165 | 
166 | If you're using the MCP server with AI agents, you can enable dynamic discovery of toolsets at runtime:
167 | 
168 | Enabling via CLI:
169 | 
170 | ```
171 | --dynamic-toolsets
172 | ```
173 | 
174 | Or via environment variable::
175 | 
176 | ```
177 | -e DYNAMIC_TOOLSETS=1 \
178 | ```
179 | 
180 | With dynamic toolsets enabled, the LLM will be able to list and activate toolsets on demand via tool interface.
181 | 
182 | ## Available Tools
183 | 
184 | ### Toolset: `space`
185 | Tools for managing Backlog space settings and general information.
186 | - `get_space`: Returns information about the Backlog space.
187 | - `get_users`: Returns list of users in the Backlog space.
188 | - `get_myself`: Returns information about the authenticated user.
189 | 
190 | ### Toolset: `project`
191 | Tools for managing projects, categories, custom fields, and issue types.
192 | - `get_project_list`: Returns list of projects.
193 | - `add_project`: Creates a new project.
194 | - `get_project`: Returns information about a specific project.
195 | - `update_project`: Updates an existing project.
196 | - `delete_project`: Deletes a project.
197 | 
198 | ### Toolset: `issue`
199 | Tools for managing issues, their comments, and related items like priorities, categories, custom fields, issue types, resolutions, and watching lists.
200 | - `get_issue`: Returns information about a specific issue.
201 | - `get_issues`: Returns list of issues.
202 | - `count_issues`: Returns count of issues.
203 | - `add_issue`: Creates a new issue in the specified project.
204 | - `update_issue`: Updates an existing issue.
205 | - `delete_issue`: Deletes an issue.
206 | - `get_issue_comments`: Returns list of comments for an issue.
207 | - `add_issue_comment`: Adds a comment to an issue.
208 | - `get_priorities`: Returns list of priorities.
209 | - `get_categories`: Returns list of categories for a project.
210 | - `get_custom_fields`: Returns list of custom fields for a project.
211 | - `get_issue_types`: Returns list of issue types for a project.
212 | - `get_resolutions`: Returns list of issue resolutions.
213 | - `get_watching_list_items`: Returns list of watching items for a user.
214 | - `get_watching_list_count`: Returns count of watching items for a user.
215 | - `get_version_milestone_list`: Returns list of version milestones for a project.
216 | - `add_version_milestone`: Creates a new version milestone for a project.
217 | - `update_version_milestone`: Updates an existing version milestone.
218 | - `delete_version_milestone`: Deletes a version milestone.
219 | 
220 | ### Toolset: `wiki`
221 | Tools for managing wiki pages.
222 | - `get_wiki_pages`: Returns list of Wiki pages.
223 | - `get_wikis_count`: Returns count of wiki pages in a project.
224 | - `get_wiki`: Returns information about a specific wiki page.
225 | - `add_wiki`: Creates a new wiki page.
226 | 
227 | ### Toolset: `git`
228 | Tools for managing Git repositories and pull requests.
229 | - `get_git_repositories`: Returns list of Git repositories for a project.
230 | - `get_git_repository`: Returns information about a specific Git repository.
231 | - `get_pull_requests`: Returns list of pull requests for a repository.
232 | - `get_pull_requests_count`: Returns count of pull requests for a repository.
233 | - `get_pull_request`: Returns information about a specific pull request.
234 | - `add_pull_request`: Creates a new pull request.
235 | - `update_pull_request`: Updates an existing pull request.
236 | - `get_pull_request_comments`: Returns list of comments for a pull request.
237 | - `add_pull_request_comment`: Adds a comment to a pull request.
238 | - `update_pull_request_comment`: Updates a comment on a pull request.
239 | 
240 | ### Toolset: `notifications`
241 | Tools for managing user notifications.
242 | - `get_notifications`: Returns list of notifications.
243 | - `get_notifications_count`: Returns count of notifications.
244 | - `reset_unread_notification_count`: Resets unread notification count.
245 | - `mark_notification_as_read`: Marks a notification as read.
246 | 
247 | ### Toolset: `document`
248 | Tools for managing documents and document trees in Backlog projects.
249 | - `get_document_tree`: Returns the hierarchical tree of documents for a project, including folders and ne
250 | - `get_documents`: Returns a flat list of documents in a project or folder.
251 | - `get_document`: Returns detailed information about a specific document, including metadata, content, an
252 | 
253 | ## Usage Examples
254 | 
255 | Once the MCP server is configured in AI agents, you can use the tools directly in your conversations. Here are some examples:
256 | 
257 | - Listing Projects
258 | ```
259 | Could you list all my Backlog projects?
260 | ```
261 | - Creating a New Issue
262 | ```
263 | Create a new bug issue in the PROJECT-KEY project with high priority titled "Fix login page error"
264 | ```
265 | - Getting Project Details
266 | ```
267 | Show me the details of the PROJECT-KEY project
268 | ```
269 | - Working with Git Repositories
270 | ```
271 | List all Git repositories in the PROJECT-KEY project
272 | ```
273 | - Managing Pull Requests
274 | ```
275 | Show me all open pull requests in the repository "repo-name" of PROJECT-KEY project
276 | ```
277 | ```
278 | Create a new pull request from branch "feature/new-feature" to "main" in the repository "repo-name" of PROJECT-KEY project
279 | ```
280 | - Watching Items
281 | ```
282 | Show me all items I'm watching 
283 | ```
284 | 
285 | ### i18n / Overriding Descriptions
286 | 
287 | You can override the descriptions of tools by creating a `.backlog-mcp-serverrc.json` file in your **home directory**.
288 | 
289 | The file should contain a JSON object with the tool names as keys and the new descriptions as values.  
290 | For example:
291 | 
292 | ```json
293 | {
294 |   "TOOL_ADD_ISSUE_COMMENT_DESCRIPTION": "An alternative description",
295 |   "TOOL_CREATE_PROJECT_DESCRIPTION": "Create a new project in Backlog"
296 | }
297 | ```
298 | 
299 | When the server starts, it determines the final description for each tool based on the following priority:
300 | 
301 | 1. Environment variables (e.g., `BACKLOG_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION`)
302 | 2. Entries in `.backlog-mcp-serverrc.json` - Supported configuration file formats: .json, .yaml, .yml
303 | 3. Built-in fallback values (English)
304 | 
305 | Sample config: 
306 | 
307 | ```json
308 | {
309 |   "mcpServers": {
310 |     "backlog": {
311 |       "command": "docker",
312 |       "args": [
313 |         "run",
314 |         "-i",
315 |         "--rm",
316 |         "-e", "BACKLOG_DOMAIN",
317 |         "-e", "BACKLOG_API_KEY",
318 |         "-v", "/yourcurrentdir/.backlog-mcp-serverrc.json:/root/.backlog-mcp-serverrc.json:ro",
319 |         "ghcr.io/nulab/backlog-mcp-server"
320 |       ],
321 |       "env": {
322 |         "BACKLOG_DOMAIN": "your-domain.backlog.com",
323 |         "BACKLOG_API_KEY": "your-api-key"
324 |       }
325 |     }
326 |   }
327 | }
328 | ```
329 | 
330 | ### Exporting Current Translations
331 | 
332 | You can export the current default translations (including any overrides) by running the binary with the --export-translations flag.
333 | 
334 | This will print all tool descriptions to stdout, including any customizations you have made.
335 | 
336 | Example:
337 | 
338 | ```bash
339 | docker run -i --rm ghcr.io/nulab/backlog-mcp-server node build/index.js --export-translations
340 | ```
341 | 
342 | or 
343 | 
344 | ```bash
345 | npx github:nulab/backlog-mcp-server --export-translations
346 | ```
347 | 
348 | ### Using a Japanese Translation Template
349 | A sample Japanese configuration file is provided at:
350 | 
351 | ```bash
352 | translationConfig/.backlog-mcp-serverrc.json.example
353 | ```
354 | 
355 | To use it, copy it to your home directory as .backlog-mcp-serverrc.json:
356 | 
357 | You can then edit the file to customize the descriptions as needed.
358 | 
359 | ### Using Environment Variables
360 | Alternatively, you can override tool descriptions via environment variables.
361 | 
362 | The environment variable names are based on the tool keys, prefixed with BACKLOG_MCP_ and written in uppercase.
363 | 
364 | Example:
365 | To override the TOOL_ADD_ISSUE_COMMENT_DESCRIPTION:
366 | 
367 | ```json
368 | {
369 |   "mcpServers": {
370 |     "backlog": {
371 |       "command": "docker",
372 |       "args": [
373 |         "run",
374 |         "-i",
375 |         "--rm",
376 |         "-e", "BACKLOG_DOMAIN",
377 |         "-e", "BACKLOG_API_KEY",
378 |         "-e", "BACKLOG_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION"
379 |         "ghcr.io/nulab/backlog-mcp-server"
380 |       ],
381 |       "env": {
382 |         "BACKLOG_DOMAIN": "your-domain.backlog.com",
383 |         "BACKLOG_API_KEY": "your-api-key",
384 |         "BACKLOG_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION": "An alternative description"
385 |       }
386 |     }
387 |   }
388 | }
389 | ```
390 | 
391 | The server loads the config file synchronously at startup.
392 | 
393 | Environment variables always take precedence over the config file.
394 | 
395 | ## Advanced Features
396 | 
397 | ### Tool Name Prefixing
398 | 
399 | Add prefix to tool names with:
400 | 
401 | ```
402 | --prefix backlog_
403 | ```
404 | 
405 | or via environment variable:
406 | 
407 | ```
408 | PREFIX="backlog_"
409 | ```
410 | 
411 | This is especially useful if you're using multiple MCP servers or tools in the same environment and want to avoid name collisions. For example, get_project can become backlog_get_project to distinguish it from similarly named tools provided by other services.
412 | 
413 | ### Response Optimization & Token Limits
414 | 
415 | #### Field Selection (GraphQL-style)
416 | 
417 | ```
418 | --optimize-response
419 | ```
420 | 
421 | Or environment variable:
422 | 
423 | ```
424 | OPTIMIZE_RESPONSE=1
425 | ```
426 | 
427 | Then, request only specific fields:
428 | 
429 | ```
430 | get_project(projectIdOrKey: "PROJECT-KEY", fields: "{ name key description }")
431 | ```
432 | 
433 | The AI will use field selection to optimize the response:
434 | 
435 | ```
436 | get_project(projectIdOrKey: "PROJECT-KEY", fields: "{ name key description }")
437 | ```
438 | 
439 | Benefits:
440 | - Reduce response size by requesting only needed fields
441 | - Focus on specific data points
442 | - Improve performance for large responses
443 | 
444 | #### Token Limiting
445 | 
446 | Large responses are automatically limited to prevent exceeding token limits:
447 | - Default limit: 50,000 tokens
448 | - Configurable via `MAX_TOKENS` environment variable
449 | - Responses exceeding the limit are truncated with a message
450 | 
451 | You can change this using:
452 | 
453 | ```
454 | MAX_TOKENS=10000
455 | ```
456 | 
457 | If a response exceeds the limit, it will be truncated with a warning.
458 | > Note: This is a best-effort mitigation, not a guaranteed enforcement.
459 | 
460 | ### Full Custom Configuration Example
461 | 
462 | This section demonstrates advanced configuration using multiple environment variables. These are experimental features and may not be supported across all MCP clients. This is not part of the MCP standard specification and should be used with caution.
463 | 
464 | ```json
465 | {
466 |   "mcpServers": {
467 |     "backlog": {
468 |       "command": "docker",
469 |       "args": [
470 |         "run",
471 |         "-i",
472 |         "--rm",
473 |         "-e", "BACKLOG_DOMAIN",
474 |         "-e", "BACKLOG_API_KEY",
475 |         "-e", "MAX_TOKENS",
476 |         "-e", "OPTIMIZE_RESPONSE",
477 |         "-e", "PREFIX",
478 |         "-e", "ENABLE_TOOLSETS",
479 |         "ghcr.io/nulab/backlog-mcp-server"
480 |       ],
481 |       "env": {
482 |         "BACKLOG_DOMAIN": "your-domain.backlog.com",
483 |         "BACKLOG_API_KEY": "your-api-key",
484 |         "MAX_TOKENS": "10000",
485 |         "OPTIMIZE_RESPONSE": "1",
486 |         "PREFIX": "backlog_",
487 |         "ENABLE_TOOLSETS": "space,project,issue",
488 |         "ENABLE_DYNAMIC_TOOLSETS": "1"
489 |       }
490 |     }
491 |   }
492 | }
493 | ```
494 | 
495 | ## Development
496 | 
497 | ### Running Tests
498 | 
499 | ```bash
500 | npm test
501 | ```
502 | 
503 | ### Adding New Tools
504 | 
505 | 1. Create a new file in `src/tools/` following the pattern of existing tools
506 | 2. Create a corresponding test file
507 | 3. Add the new tool to `src/tools/tools.ts`
508 | 4. Build and test your changes
509 | 
510 | ### Command Line Options
511 | 
512 | The server supports several command line options:
513 | 
514 | - `--export-translations`: Export all translation keys and values
515 | - `--optimize-response`: Enable GraphQL-style field selection
516 | - `--max-tokens=NUMBER`: Set maximum token limit for responses
517 | - `--prefix=STRING`: Optional string prefix to prepend to all tool names (default: "")
518 | - `--enable-toolsets <toolsets...>`: Specify which toolsets to enable (comma-separated or multiple arguments). Defaults to "all".
519 |   Example: `--enable-toolsets space,project` or `--enable-toolsets issue --enable-toolsets git`
520 |   Available toolsets: `space`, `project`, `issue`, `wiki`, `git`, `notifications`.
521 | 
522 | Example:
523 | ```bash
524 | node build/index.js --optimize-response --max-tokens=100000 --prefix="backlog_" --enable-toolsets space,issue
525 | ```
526 | 
527 | ## License
528 | 
529 | This project is licensed under the [MIT License](./LICENSE).
530 | 
531 | Please note: This tool is provided under the MIT License **without any warranty or official support**.  
532 | Use it at your own risk after reviewing the contents and determining its suitability for your needs.  
533 | If you encounter any issues, please report them via [GitHub Issues](../../issues).
534 | 
```

--------------------------------------------------------------------------------
/src/version.template.ts:
--------------------------------------------------------------------------------

```typescript
1 | export const VERSION = '__VERSION__';
2 | 
```

--------------------------------------------------------------------------------
/src/types/mcp.ts:
--------------------------------------------------------------------------------

```typescript
1 | export type MCPOptions = {
2 |   useFields: boolean;
3 |   maxTokens: number;
4 |   prefix: string;
5 | };
6 | 
```

--------------------------------------------------------------------------------
/src/types/result.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export type ErrorLike = {
 2 |   kind: 'error';
 3 |   message: string;
 4 | };
 5 | 
 6 | export type Success<T> = {
 7 |   kind: 'ok';
 8 |   data: T;
 9 | };
10 | 
11 | export type SafeResult<T> = Success<T> | ErrorLike;
12 | 
13 | export function isErrorLike<T>(res: SafeResult<T>): res is ErrorLike {
14 |   return res.kind === 'error';
15 | }
16 | 
```

--------------------------------------------------------------------------------
/src/backlog/backlogErrorHandler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ErrorLike } from '../types/result.js';
 2 | import { parseBacklogAPIError } from './parseBacklogAPIError.js';
 3 | 
 4 | export const backlogErrorHandler = (err: unknown): ErrorLike => {
 5 |   const parsed = parseBacklogAPIError(err);
 6 |   return {
 7 |     kind: 'error',
 8 |     message: parsed.message,
 9 |   };
10 | };
11 | 
```

--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | // jest.config.js
 2 | export default {
 3 |     preset: 'ts-jest/presets/default-esm',
 4 |     transform: {
 5 |       '^.+\\.tsx?$': ['ts-jest', { useESM: true }],
 6 |     },
 7 |     extensionsToTreatAsEsm: ['.ts'],
 8 |     testEnvironment: 'node',
 9 |     moduleNameMapper: {
10 |       '^(\\.{1,2}/.*)\\.js$': '$1',
11 |     },
12 |   };
13 |   
```

--------------------------------------------------------------------------------
/src/handlers/transformers/wrapWithErrorHandling.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ErrorLike, SafeResult } from '../../types/result.js';
 2 | import { runToolSafely } from '../../utils/runToolSafely.js';
 3 | 
 4 | export function wrapWithErrorHandling<I, O>(
 5 |   fn: (input: I) => Promise<O>,
 6 |   onError?: (err: unknown) => ErrorLike
 7 | ): (input: I) => Promise<SafeResult<O>> {
 8 |   return runToolSafely(fn, onError);
 9 | }
10 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Build stage
 2 | FROM node:22 AS builder
 3 | 
 4 | WORKDIR /app
 5 | COPY package*.json ./
 6 | RUN npm ci
 7 | 
 8 | COPY . .
 9 | RUN npm run build
10 | 
11 | # Runtime stage
12 | FROM node:22-slim AS runner
13 | 
14 | WORKDIR /app
15 | COPY --from=builder /app/node_modules ./node_modules
16 | COPY --from=builder /app/build ./build
17 | COPY --from=builder /app/package.json ./
18 | 
19 | ARG VERSION
20 | ENV APP_VERSION=$VERSION
21 | 
22 | CMD ["node", "build/index.js"]
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "compilerOptions": {
 3 |       "target": "ES2022",
 4 |       "module": "Node16",
 5 |       "moduleResolution": "Node16",
 6 |       "outDir": "./build",
 7 |       "rootDir": "./src",
 8 |       "strict": true,
 9 |       "esModuleInterop": true,
10 |       "skipLibCheck": true,
11 |       "forceConsistentCasingInFileNames": true,
12 |       "types": ["@jest/globals"],
13 |       "isolatedModules": true
14 |     },
15 |     "include": ["src/**/*"],
16 |     "exclude": ["node_modules", "src/**/*.test.ts"]
17 |   }
```

--------------------------------------------------------------------------------
/src/utils/tokenCounter.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export function countTokens(text: string): number {
 2 |   // Normalize whitespace (convert tabs and newlines to spaces)
 3 |   const normalized = text
 4 |     .replace(/\s+/g, ' ') // Replace multiple whitespace with a single space
 5 |     .replace(/[\n\t]/g, ' ') // Replace newlines and tabs with a space
 6 |     .trim();
 7 | 
 8 |   // Split into words and individual symbols
 9 |   const tokens = normalized.match(/\w+|[^\s\w]/g);
10 | 
11 |   // Return the number of tokens
12 |   return tokens ? tokens.length : 0;
13 | }
14 | 
```

--------------------------------------------------------------------------------
/scripts/replace-version.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { readFileSync, writeFileSync, copyFileSync } from "fs";
 2 | 
 3 | const pkg = JSON.parse(readFileSync("./package.json", "utf8"));
 4 | const version = pkg.version;
 5 | 
 6 | const templatePath = "./src/version.template.ts";
 7 | const outputPath = "./src/version.ts";
 8 | 
 9 | // Always reset from template before injecting
10 | copyFileSync(templatePath, outputPath);
11 | 
12 | const content = readFileSync(outputPath, "utf8");
13 | const replaced = content.replace(/__VERSION__/, version);
14 | writeFileSync(outputPath, replaced);
15 | 
16 | console.log(`✔ Injected VERSION=${version} into ${outputPath}`);
17 | 
```

--------------------------------------------------------------------------------
/src/types/toolsets.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { DynamicToolDefinition, ToolDefinition } from './tool.js';
 2 | 
 3 | type BaseToolset<TTool> = {
 4 |   name: string;
 5 |   description: string;
 6 |   enabled: boolean;
 7 |   tools: TTool[];
 8 | };
 9 | 
10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 | export type Toolset = BaseToolset<ToolDefinition<any, any>>;
12 | export type ToolsetGroup = { toolsets: Toolset[] };
13 | 
14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
15 | export type DynamicToolset = BaseToolset<DynamicToolDefinition<any>>;
16 | export type DynamicToolsetGroup = { toolsets: DynamicToolset[] };
17 | 
```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import pino from 'pino';
 2 | 
 3 | if (!process.env.NODE_ENV) {
 4 |   process.env.NODE_ENV = 'production';
 5 | }
 6 | 
 7 | const isProd = process.env.NODE_ENV === 'production';
 8 | 
 9 | export const logger = pino(
10 |   {
11 |     level: isProd ? 'error' : 'debug',
12 |     transport: isProd
13 |       ? undefined
14 |       : {
15 |           target: 'pino-pretty',
16 |           options: {
17 |             destination: 2,
18 |             colorize: true,
19 |             translateTime: 'SYS:yyyy-mm-dd HH:MM:ss.l',
20 |             ignore: 'pid,hostname',
21 |             singleLine: true,
22 |           },
23 |         },
24 |   },
25 |   isProd ? pino.destination({ dest: 2, sync: false }) : undefined
26 | );
27 | 
```

--------------------------------------------------------------------------------
/src/utils/runToolSafely.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ErrorLike, SafeResult } from '../types/result.js';
 2 | 
 3 | /**
 4 |  * Runs a tool handler safely, catching any errors and converting to SafeResult.
 5 |  * The `onError` handler defines how to turn unknown errors into ErrorLike objects.
 6 |  */
 7 | export function runToolSafely<I, O>(
 8 |   fn: (input: I) => Promise<O>,
 9 |   onError?: (err: unknown) => ErrorLike
10 | ): (input: I) => Promise<SafeResult<O>> {
11 |   return async (input: I) => {
12 |     try {
13 |       const data = await fn(input);
14 |       return { kind: 'ok', data };
15 |     } catch (err) {
16 |       if (onError) {
17 |         return onError(err);
18 |       }
19 |       return { kind: 'error', message: 'Unknown: ' + err };
20 |     }
21 |   };
22 | }
23 | 
```

--------------------------------------------------------------------------------
/src/backlog/customFields.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export type CustomFieldInput = {
 2 |   id: number;
 3 |   value: string | number | string[];
 4 |   otherValue?: string;
 5 | };
 6 | 
 7 | /**
 8 |  * Converts Backlog-style customFields array into proper payload format
 9 |  */
10 | export function customFieldsToPayload(
11 |   customFields: CustomFieldInput[] | undefined
12 | ): Record<string, string | number | string[] | undefined> {
13 |   if (customFields == null) {
14 |     return {};
15 |   }
16 |   const result: Record<string, string | number | string[] | undefined> = {};
17 | 
18 |   for (const field of customFields) {
19 |     result[`customField_${field.id}`] = field.value;
20 |     if (field.otherValue) {
21 |       result[`customField_${field.id}_otherValue`] = field.otherValue;
22 |     }
23 |   }
24 | 
25 |   return result;
26 | }
27 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI 
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 |   pull_request:
 8 |     branches:
 9 |       - main
10 | jobs:
11 |   ci:
12 |     if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')
13 |     runs-on: ubuntu-latest
14 |     name: 🧪 Lint, Test, Build
15 | 
16 |     steps:
17 |       - name: 📥 Checkout
18 |         uses: actions/checkout@v3
19 | 
20 |       - name: 🟢 Setup Node.js
21 |         uses: actions/setup-node@v4
22 |         with:
23 |           node-version: '22'
24 |           cache: 'npm'
25 | 
26 |       - name: 📦 Install deps
27 |         run: npm ci
28 | 
29 |       - name: 🔍 Lint (if exists)
30 |         run: npm run lint
31 | 
32 |       - name: 🎨 Format check (Prettier)
33 |         run: npm run format
34 | 
35 |       - name: 🧪 Run tests
36 |         run: npm test
37 | 
38 |       - name: 🛠 Build
39 |         run: npm run build
40 | 
```

--------------------------------------------------------------------------------
/src/utils/toolRegistrar.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { registerTools } from '../registerTools.js';
 2 | import { MCPOptions } from '../types/mcp.js';
 3 | import { ToolRegistrar } from '../types/tool.js';
 4 | import { ToolsetGroup } from '../types/toolsets.js';
 5 | import { enableToolset } from '../utils/toolsetUtils.js';
 6 | import { BacklogMCPServer } from './wrapServerWithToolRegistry.js';
 7 | 
 8 | export function createToolRegistrar(
 9 |   server: BacklogMCPServer,
10 |   toolsetGroup: ToolsetGroup,
11 |   options: MCPOptions
12 | ): ToolRegistrar {
13 |   return {
14 |     async enableToolsetAndRefresh(toolset: string): Promise<string> {
15 |       const msg = enableToolset(toolsetGroup, toolset);
16 |       registerTools(server, toolsetGroup, options);
17 |       await server.server.sendToolListChanged();
18 |       return msg;
19 |     },
20 |   };
21 | }
22 | 
```

--------------------------------------------------------------------------------
/src/tools/getPriorities.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { PrioritySchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getPrioritiesSchema = buildToolSchema((_t) => ({}));
 8 | 
 9 | export const getPrioritiesTool = (
10 |   backlog: Backlog,
11 |   { t }: TranslationHelper
12 | ): ToolDefinition<
13 |   ReturnType<typeof getPrioritiesSchema>,
14 |   (typeof PrioritySchema)['shape']
15 | > => {
16 |   return {
17 |     name: 'get_priorities',
18 |     description: t(
19 |       'TOOL_GET_PRIORITIES_DESCRIPTION',
20 |       'Returns list of priorities'
21 |     ),
22 |     schema: z.object(getPrioritiesSchema(t)),
23 |     outputSchema: PrioritySchema,
24 |     handler: async () => backlog.getPriorities(),
25 |   };
26 | };
27 | 
```

--------------------------------------------------------------------------------
/src/tools/getResolutions.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { ResolutionSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getResolutionsSchema = buildToolSchema((_t) => ({}));
 8 | 
 9 | export const getResolutionsTool = (
10 |   backlog: Backlog,
11 |   { t }: TranslationHelper
12 | ): ToolDefinition<
13 |   ReturnType<typeof getResolutionsSchema>,
14 |   (typeof ResolutionSchema)['shape']
15 | > => {
16 |   return {
17 |     name: 'get_resolutions',
18 |     description: t(
19 |       'TOOL_GET_RESOLUTIONS_DESCRIPTION',
20 |       'Returns list of issue resolutions'
21 |     ),
22 |     schema: z.object(getResolutionsSchema(t)),
23 |     outputSchema: ResolutionSchema,
24 |     handler: async () => backlog.getResolutions(),
25 |   };
26 | };
27 | 
```

--------------------------------------------------------------------------------
/src/tools/getUsers.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { UserSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getUsersSchema = buildToolSchema((_t) => ({}));
 8 | 
 9 | export const getUsersTool = (
10 |   backlog: Backlog,
11 |   { t }: TranslationHelper
12 | ): ToolDefinition<
13 |   ReturnType<typeof getUsersSchema>,
14 |   (typeof UserSchema)['shape']
15 | > => {
16 |   return {
17 |     name: 'get_users',
18 |     description: t(
19 |       'TOOL_GET_USERS_DESCRIPTION',
20 |       'Returns list of users in the Backlog space'
21 |     ),
22 |     schema: z.object(getUsersSchema(t)),
23 |     outputSchema: UserSchema,
24 |     importantFields: ['userId', 'name', 'roleType', 'lang'],
25 |     handler: async () => backlog.getUsers(),
26 |   };
27 | };
28 | 
```

--------------------------------------------------------------------------------
/src/tools/getSpace.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { SpaceSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getSpaceSchema = buildToolSchema((_t) => ({}));
 8 | 
 9 | export const getSpaceTool = (
10 |   backlog: Backlog,
11 |   { t }: TranslationHelper
12 | ): ToolDefinition<
13 |   ReturnType<typeof getSpaceSchema>,
14 |   (typeof SpaceSchema)['shape']
15 | > => {
16 |   return {
17 |     name: 'get_space',
18 |     description: t(
19 |       'TOOL_GET_SPACE_DESCRIPTION',
20 |       'Returns information about the Backlog space'
21 |     ),
22 |     schema: z.object(getSpaceSchema(t)),
23 |     outputSchema: SpaceSchema,
24 |     importantFields: ['spaceKey', 'name', 'lang', 'timezone'],
25 |     handler: async () => backlog.getSpace(),
26 |   };
27 | };
28 | 
```

--------------------------------------------------------------------------------
/src/tools/getMyself.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { UserSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getMyselfSchema = buildToolSchema((_t) => ({}));
 8 | 
 9 | export const getMyselfTool = (
10 |   backlog: Backlog,
11 |   { t }: TranslationHelper
12 | ): ToolDefinition<
13 |   ReturnType<typeof getMyselfSchema>,
14 |   (typeof UserSchema)['shape']
15 | > => {
16 |   return {
17 |     name: 'get_myself',
18 |     description: t(
19 |       'TOOL_GET_MYSELF_DESCRIPTION',
20 |       'Returns information about the authenticated user'
21 |     ),
22 |     schema: z.object(getMyselfSchema(t)),
23 |     outputSchema: UserSchema,
24 |     importantFields: ['id', 'userId', 'name', 'roleType'],
25 |     handler: async () => backlog.getMyself(),
26 |   };
27 | };
28 | 
```

--------------------------------------------------------------------------------
/src/handlers/transformers/wrapWithTokenLimit.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { SafeResult } from '../../types/result.js';
 2 | import { countTokens } from '../../utils/tokenCounter.js';
 3 | 
 4 | export function wrapWithTokenLimit<I, O>(
 5 |   fn: (input: I) => Promise<SafeResult<O>>,
 6 |   maxTokens: number
 7 | ): (input: I) => Promise<SafeResult<string>> {
 8 |   return async (input: I) => {
 9 |     const result = await fn(input);
10 |     if (
11 |       result == null ||
12 |       typeof result !== 'object' ||
13 |       result.kind == 'error'
14 |     ) {
15 |       return result;
16 |     }
17 | 
18 |     const fullText = JSON.stringify(result.data, null, 2);
19 |     const tokenCount = countTokens(fullText);
20 | 
21 |     if (tokenCount > maxTokens) {
22 |       const roughCut = fullText.slice(0, Math.floor(maxTokens * 4));
23 |       return {
24 |         kind: 'ok',
25 |         data: `${roughCut}\n...(output truncated due to token limit)`,
26 |       };
27 |     }
28 | 
29 |     return { kind: 'ok', data: fullText };
30 |   };
31 | }
32 | 
```

--------------------------------------------------------------------------------
/src/tools/resetUnreadNotificationCount.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { NotificationCountSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const resetUnreadNotificationCountSchema = buildToolSchema((_t) => ({}));
 8 | 
 9 | export const resetUnreadNotificationCountTool = (
10 |   backlog: Backlog,
11 |   { t }: TranslationHelper
12 | ): ToolDefinition<
13 |   ReturnType<typeof resetUnreadNotificationCountSchema>,
14 |   (typeof NotificationCountSchema)['shape']
15 | > => {
16 |   return {
17 |     name: 'reset_unread_notification_count',
18 |     description: t(
19 |       'TOOL_RESET_UNREAD_NOTIFICATION_COUNT_DESCRIPTION',
20 |       'Reset unread notification count'
21 |     ),
22 |     schema: z.object(resetUnreadNotificationCountSchema(t)),
23 |     outputSchema: NotificationCountSchema,
24 |     handler: async () => backlog.resetNotificationsMarkAsRead(),
25 |   };
26 | };
27 | 
```

--------------------------------------------------------------------------------
/src/tools/getDocument.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Backlog } from 'backlog-js';
 2 | import { z } from 'zod';
 3 | import { TranslationHelper } from '../createTranslationHelper.js';
 4 | import { DocumentItemSchema } from '../types/zod/backlogOutputDefinition.js';
 5 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 6 | 
 7 | const getDocumentSchema = buildToolSchema((t) => ({
 8 |   documentId: z
 9 |     .string()
10 |     .describe(t('TOOL_GET_DOCUMENT_DOCUMENT_ID', 'Document ID')),
11 | }));
12 | 
13 | export const getDocumentTool = (
14 |   backlog: Backlog,
15 |   { t }: TranslationHelper
16 | ): ToolDefinition<
17 |   ReturnType<typeof getDocumentSchema>,
18 |   (typeof DocumentItemSchema)['shape']
19 | > => {
20 |   return {
21 |     name: 'get_document',
22 |     description: t(
23 |       'TOOL_GET_DOCUMENT_DESCRIPTION',
24 |       'Gets information about a document.'
25 |     ),
26 |     schema: z.object(getDocumentSchema(t)),
27 |     outputSchema: DocumentItemSchema,
28 |     importantFields: ['id', 'title', 'createdUser'],
29 |     handler: async ({ documentId }) => {
30 |       return backlog.getDocument(documentId);
31 |     },
32 |   };
33 | };
34 | 
```

--------------------------------------------------------------------------------
/src/tools/getWatchingListItems.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { WatchingListItemSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getWatchingListItemsSchema = buildToolSchema((t) => ({
 8 |   userId: z
 9 |     .number()
10 |     .describe(t('TOOL_GET_WATCHING_LIST_ITEMS_USER_ID', 'User ID')),
11 | }));
12 | 
13 | export const getWatchingListItemsTool = (
14 |   backlog: Backlog,
15 |   { t }: TranslationHelper
16 | ): ToolDefinition<
17 |   ReturnType<typeof getWatchingListItemsSchema>,
18 |   (typeof WatchingListItemSchema)['shape']
19 | > => {
20 |   return {
21 |     name: 'get_watching_list_items',
22 |     description: t(
23 |       'TOOL_GET_WATCHING_LIST_ITEMS_DESCRIPTION',
24 |       'Returns list of watching items for a user'
25 |     ),
26 |     schema: z.object(getWatchingListItemsSchema(t)),
27 |     outputSchema: WatchingListItemSchema,
28 |     handler: async ({ userId }) => backlog.getWatchingListItems(userId),
29 |   };
30 | };
31 | 
```

--------------------------------------------------------------------------------
/src/tools/getWatchingListCount.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { WatchingListCountSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getWatchingListCountSchema = buildToolSchema((t) => ({
 8 |   userId: z
 9 |     .number()
10 |     .describe(t('TOOL_GET_WATCHING_LIST_COUNT_USER_ID', 'User ID')),
11 | }));
12 | 
13 | export const getWatchingListCountTool = (
14 |   backlog: Backlog,
15 |   { t }: TranslationHelper
16 | ): ToolDefinition<
17 |   ReturnType<typeof getWatchingListCountSchema>,
18 |   (typeof WatchingListCountSchema)['shape']
19 | > => {
20 |   return {
21 |     name: 'get_watching_list_count',
22 |     description: t(
23 |       'TOOL_GET_WATCHING_LIST_COUNT_DESCRIPTION',
24 |       'Returns count of watching items for a user'
25 |     ),
26 |     schema: z.object(getWatchingListCountSchema(t)),
27 |     outputSchema: WatchingListCountSchema,
28 |     handler: async ({ userId }) => backlog.getWatchingListCount(userId),
29 |   };
30 | };
31 | 
```

--------------------------------------------------------------------------------
/src/types/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { TranslationHelper } from '../createTranslationHelper.js';
 3 | import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
 4 | 
 5 | export type ToolDefinition<
 6 |   Shape extends z.ZodRawShape,
 7 |   OutputShape extends z.ZodRawShape,
 8 | > = {
 9 |   name: string;
10 |   description: string;
11 |   schema: z.ZodObject<Shape>;
12 |   outputSchema: z.ZodObject<OutputShape>;
13 |   handler: (
14 |     input: z.infer<z.ZodObject<Shape>> & { fields?: string }
15 |   ) => Promise<
16 |     z.infer<z.ZodObject<OutputShape>> | z.infer<z.ZodObject<OutputShape>>[]
17 |   >;
18 |   importantFields?: (keyof z.infer<z.ZodObject<OutputShape>>)[];
19 | };
20 | 
21 | export const buildToolSchema = <T extends z.ZodRawShape>(
22 |   fn: (t: TranslationHelper['t']) => T
23 | ) => fn;
24 | 
25 | export type DynamicToolDefinition<Shape extends z.ZodRawShape> = {
26 |   name: string;
27 |   description: string;
28 |   schema: z.ZodObject<Shape>;
29 |   handler: (input: z.infer<z.ZodObject<Shape>>) => Promise<CallToolResult>;
30 | };
31 | 
32 | export interface ToolRegistrar {
33 |   enableToolsetAndRefresh(toolset: string): Promise<string>;
34 | }
35 | 
```

--------------------------------------------------------------------------------
/src/handlers/transformers/wrapWithToolResult.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
 2 | import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
 3 | import { isErrorLike, SafeResult } from '../../types/result.js';
 4 | 
 5 | /**
 6 |  * Convert SafeResult<T> to CallToolResult
 7 |  */
 8 | export function wrapWithToolResult<I, T>(
 9 |   fn: (input: I) => Promise<SafeResult<string | T>>
10 | ): (input: I, extra: RequestHandlerExtra) => Promise<CallToolResult> {
11 |   return async (input: I, _extra) => {
12 |     const result = await fn(input);
13 | 
14 |     if (isErrorLike(result)) {
15 |       return {
16 |         isError: true,
17 |         content: [
18 |           {
19 |             type: 'text',
20 |             text: result.message,
21 |           },
22 |         ],
23 |       };
24 |     }
25 | 
26 |     const data = result.data;
27 | 
28 |     if (typeof data === 'string') {
29 |       return {
30 |         content: [
31 |           {
32 |             type: 'text',
33 |             text: data,
34 |           },
35 |         ],
36 |       };
37 |     }
38 | 
39 |     return {
40 |       content: [
41 |         {
42 |           type: 'text',
43 |           text: JSON.stringify(data, null, 2),
44 |         },
45 |       ],
46 |     };
47 |   };
48 | }
49 | 
```

--------------------------------------------------------------------------------
/src/tools/getWiki.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { WikiSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getWikiSchema = buildToolSchema((t) => ({
 8 |   wikiId: z
 9 |     .union([z.string(), z.number()])
10 |     .describe(t('TOOL_GET_WIKI_ID', 'Wiki ID')),
11 | }));
12 | 
13 | export const getWikiTool = (
14 |   backlog: Backlog,
15 |   { t }: TranslationHelper
16 | ): ToolDefinition<
17 |   ReturnType<typeof getWikiSchema>,
18 |   (typeof WikiSchema)['shape']
19 | > => {
20 |   return {
21 |     name: 'get_wiki',
22 |     description: t(
23 |       'TOOL_GET_WIKI_DESCRIPTION',
24 |       'Returns information about a specific wiki page'
25 |     ),
26 |     schema: z.object(getWikiSchema(t)),
27 |     outputSchema: WikiSchema,
28 |     importantFields: ['id', 'projectId', 'name', 'content'],
29 |     handler: async ({ wikiId }) => {
30 |       const wikiIdNumber =
31 |         typeof wikiId === 'string' ? parseInt(wikiId, 10) : wikiId;
32 |       return backlog.getWiki(wikiIdNumber);
33 |     },
34 |   };
35 | };
36 | 
```

--------------------------------------------------------------------------------
/src/tools/resetUnreadNotificationCount.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { resetUnreadNotificationCountTool } from './resetUnreadNotificationCount.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('resetUnreadNotificationCountTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     resetNotificationsMarkAsRead: jest
 9 |       .fn<() => Promise<any>>()
10 |       .mockResolvedValue({
11 |         count: 0,
12 |       }),
13 |   };
14 | 
15 |   const mockTranslationHelper = createTranslationHelper();
16 |   const tool = resetUnreadNotificationCountTool(
17 |     mockBacklog as Backlog,
18 |     mockTranslationHelper
19 |   );
20 | 
21 |   it('returns reset result as formatted JSON text', async () => {
22 |     const result = await tool.handler({});
23 | 
24 |     if (Array.isArray(result)) {
25 |       throw new Error('Unexpected array result');
26 |     }
27 | 
28 |     expect(result.count).toEqual(0);
29 |   });
30 | 
31 |   it('calls backlog.resetNotificationsMarkAsRead', async () => {
32 |     await tool.handler({});
33 | 
34 |     expect(mockBacklog.resetNotificationsMarkAsRead).toHaveBeenCalled();
35 |   });
36 | });
37 | 
```

--------------------------------------------------------------------------------
/src/tools/getWatchingListCount.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getWatchingListCountTool } from './getWatchingListCount.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getWatchingListCountTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getWatchingListCount: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       count: 42,
10 |     }),
11 |   };
12 | 
13 |   const mockTranslationHelper = createTranslationHelper();
14 |   const tool = getWatchingListCountTool(
15 |     mockBacklog as Backlog,
16 |     mockTranslationHelper
17 |   );
18 | 
19 |   it('returns watching list count as formatted JSON text', async () => {
20 |     const result = await tool.handler({
21 |       userId: 1,
22 |     });
23 | 
24 |     if (Array.isArray(result)) {
25 |       throw new Error('Unexpected array result');
26 |     }
27 | 
28 |     expect(result.count).toEqual(42);
29 |   });
30 | 
31 |   it('calls backlog.getWatchingListCount with correct params', async () => {
32 |     await tool.handler({
33 |       userId: 1,
34 |     });
35 | 
36 |     expect(mockBacklog.getWatchingListCount).toHaveBeenCalledWith(1);
37 |   });
38 | });
39 | 
```

--------------------------------------------------------------------------------
/src/tools/markNotificationAsRead.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { markNotificationAsReadTool } from './markNotificationAsRead.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('markNotificationAsReadTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     markAsReadNotification: jest
 9 |       .fn<() => Promise<void>>()
10 |       .mockResolvedValue(undefined),
11 |   };
12 | 
13 |   const mockTranslationHelper = createTranslationHelper();
14 |   const tool = markNotificationAsReadTool(
15 |     mockBacklog as Backlog,
16 |     mockTranslationHelper
17 |   );
18 | 
19 |   it('returns success message as formatted JSON text', async () => {
20 |     const result = await tool.handler({
21 |       id: 123,
22 |     });
23 | 
24 |     if (Array.isArray(result)) {
25 |       throw new Error('Unexpected array result');
26 |     }
27 |     expect(result.success).toBe(true);
28 |   });
29 | 
30 |   it('calls backlog.markAsReadNotification with correct params', async () => {
31 |     await tool.handler({
32 |       id: 123,
33 |     });
34 | 
35 |     expect(mockBacklog.markAsReadNotification).toHaveBeenCalledWith(123);
36 |   });
37 | });
38 | 
```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | import js from '@eslint/js';
 2 | import parser from '@typescript-eslint/parser';
 3 | import plugin from '@typescript-eslint/eslint-plugin';
 4 | 
 5 | /** @type {import("eslint").Linter.FlatConfig[]} */
 6 | export default [
 7 |   {
 8 |     ignores: ['build/**', 'node_modules/**'],
 9 |   },
10 |   js.configs.recommended,
11 |   {
12 |     files: ['**/*.ts'],
13 |     languageOptions: {
14 |       parser: parser,
15 |       parserOptions: {
16 |         ecmaVersion: 'latest',
17 |         sourceType: 'module',
18 |       },
19 |       globals: {
20 |         process: 'readonly',
21 |         console: 'readonly',
22 |       },
23 |     },
24 |     plugins: {
25 |       '@typescript-eslint': plugin,
26 |     },
27 |     rules: {
28 |       ...plugin.configs.recommended.rules,
29 |       '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
30 |       'no-console': ['warn', { allow: ['warn', 'error'] }]
31 |     },
32 |   },
33 |   {
34 |     files: ['**/*.test.ts'],
35 |     rules: {
36 |       '@typescript-eslint/no-explicit-any': 'off', // Allow on unit tests
37 |     },
38 |   },
39 |   {
40 |     files: ['**/*.js'],
41 |     languageOptions: {
42 |       ecmaVersion: 'latest',
43 |       sourceType: 'module',
44 |       globals: {
45 |         console: 'readonly',
46 |       },
47 |     },
48 |   }
49 | 
50 | ];
51 | 
```

--------------------------------------------------------------------------------
/src/tools/getDocumentTree.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Backlog } from 'backlog-js';
 2 | import { z } from 'zod';
 3 | import { TranslationHelper } from '../createTranslationHelper.js';
 4 | import {
 5 |   DocumentTreeFullSchema,
 6 |   DocumentTreeFullSchemaZ,
 7 | } from '../types/zod/backlogOutputDefinition.js';
 8 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 9 | 
10 | const getDocumentTreeSchema = buildToolSchema((t) => ({
11 |   projectIdOrKey: z
12 |     .union([z.string(), z.number()])
13 |     .describe(
14 |       t('TOOL_GET_DOCUMENT_TREE_PROJECT_ID_OR_KEY', 'Project ID or Key')
15 |     ),
16 | }));
17 | 
18 | export const getDocumentTreeTool = (
19 |   backlog: Backlog,
20 |   { t }: TranslationHelper
21 | ): ToolDefinition<
22 |   ReturnType<typeof getDocumentTreeSchema>,
23 |   typeof DocumentTreeFullSchema
24 | > => {
25 |   return {
26 |     name: 'get_document_tree',
27 |     description: t(
28 |       'TOOL_GET_DOCUMENT_TREE_DESCRIPTION',
29 |       'Gets the document tree of a project.'
30 |     ),
31 |     schema: z.object(getDocumentTreeSchema(t)),
32 |     outputSchema: DocumentTreeFullSchemaZ,
33 |     importantFields: ['projectId', 'activeTree', 'trashTree'],
34 |     handler: async ({ projectIdOrKey }) => {
35 |       return backlog.getDocumentTree(projectIdOrKey);
36 |     },
37 |   };
38 | };
39 | 
```

--------------------------------------------------------------------------------
/src/utils/wrapServerWithToolRegistry.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {
 2 |   McpServer,
 3 |   ToolCallback,
 4 | } from '@modelcontextprotocol/sdk/server/mcp.js';
 5 | import { z } from 'zod';
 6 | 
 7 | // Extended type that has the MCP core, a set of registered tool names, and a registration function
 8 | export interface BacklogMCPServer extends McpServer {
 9 |   __registeredToolNames?: Set<string>;
10 | 
11 |   registerOnce: (
12 |     name: string,
13 |     description: string,
14 |     schema: z.ZodRawShape,
15 |     handler: ToolCallback<z.ZodRawShape>
16 |   ) => void;
17 | }
18 | 
19 | // This function takes an McpServer instance and extends it with a tool registration mechanism that prevents duplicate tool registrations.
20 | export function wrapServerWithToolRegistry(
21 |   server: McpServer
22 | ): BacklogMCPServer {
23 |   const s = server as BacklogMCPServer;
24 | 
25 |   if (!s.__registeredToolNames) {
26 |     s.__registeredToolNames = new Set();
27 |   }
28 | 
29 |   s.registerOnce = (
30 |     name: string,
31 |     description: string,
32 |     schema: z.ZodRawShape,
33 |     handler: ToolCallback<z.ZodRawShape>
34 |   ) => {
35 |     if (s.__registeredToolNames!.has(name)) {
36 |       console.warn(`Skipping duplicate tool registration: ${name}`);
37 |       return;
38 |     }
39 |     s.__registeredToolNames!.add(name);
40 |     s.tool(name, description, schema, handler);
41 |   };
42 | 
43 |   return s;
44 | }
45 | 
```

--------------------------------------------------------------------------------
/src/createTranslationHelper.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { cosmiconfigSync } from 'cosmiconfig';
 2 | import os from 'os';
 3 | 
 4 | export interface TranslationHelper {
 5 |   t: (key: string, fallback: string) => string;
 6 |   dump: () => Record<string, string>;
 7 | }
 8 | 
 9 | export function createTranslationHelper(options?: {
10 |   configName?: string;
11 |   searchDir?: string;
12 | }): TranslationHelper {
13 |   const usedKeys: Record<string, string> = {};
14 | 
15 |   const configName = options?.configName ?? 'backlog-mcp-server';
16 | 
17 |   // Load config file
18 |   const explorer = cosmiconfigSync(configName);
19 |   const searchPath = options?.searchDir ?? os.homedir();
20 | 
21 |   const configResult = explorer.search(searchPath);
22 |   const config = configResult?.config || {};
23 | 
24 |   function toEnvKey(key: string): string {
25 |     return `BACKLOG_MCP_${key}`;
26 |   }
27 | 
28 |   function t(key: string, fallback: string): string {
29 |     const upperKey = key.toUpperCase();
30 | 
31 |     if (usedKeys[upperKey]) {
32 |       return usedKeys[upperKey];
33 |     }
34 | 
35 |     // Priority:ENV → config → fallback
36 |     const value =
37 |       process.env[toEnvKey(upperKey)] || config[upperKey] || fallback;
38 | 
39 |     usedKeys[upperKey] = value;
40 |     return value;
41 |   }
42 | 
43 |   function dump(): Record<string, string> {
44 |     return { ...usedKeys };
45 |   }
46 | 
47 |   return { t, dump };
48 | }
49 | 
```

--------------------------------------------------------------------------------
/src/tools/markNotificationAsRead.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | const markNotificationAsReadSchema = buildToolSchema((t) => ({
 7 |   id: z
 8 |     .number()
 9 |     .describe(
10 |       t('TOOL_MARK_NOTIFICATION_AS_READ_ID', 'Notification ID to mark as read')
11 |     ),
12 | }));
13 | 
14 | export const MarkNotificationAsReadResultSchema = z.object({
15 |   success: z.boolean(),
16 |   message: z.string(),
17 | });
18 | 
19 | export const markNotificationAsReadTool = (
20 |   backlog: Backlog,
21 |   { t }: TranslationHelper
22 | ): ToolDefinition<
23 |   ReturnType<typeof markNotificationAsReadSchema>,
24 |   (typeof MarkNotificationAsReadResultSchema)['shape']
25 | > => {
26 |   return {
27 |     name: 'mark_notification_as_read',
28 |     description: t(
29 |       'TOOL_MARK_NOTIFICATION_AS_READ_DESCRIPTION',
30 |       'Mark a notification as read'
31 |     ),
32 |     schema: z.object(markNotificationAsReadSchema(t)),
33 |     outputSchema: MarkNotificationAsReadResultSchema,
34 |     handler: async ({ id }) => {
35 |       await backlog.markAsReadNotification(id);
36 |       return {
37 |         success: true,
38 |         message: `Notification ${id} marked as read`,
39 |       };
40 |     },
41 |   };
42 | };
43 | 
```

--------------------------------------------------------------------------------
/src/tools/getNotificationsCount.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getNotificationsCountTool } from './getNotificationsCount.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getNotificationsCountTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getNotificationsCount: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       count: 42,
10 |     }),
11 |   };
12 | 
13 |   const mockTranslationHelper = createTranslationHelper();
14 |   const tool = getNotificationsCountTool(
15 |     mockBacklog as Backlog,
16 |     mockTranslationHelper
17 |   );
18 | 
19 |   it('returns notification count as formatted JSON text', async () => {
20 |     const result = await tool.handler({
21 |       alreadyRead: false,
22 |       resourceAlreadyRead: false,
23 |     });
24 | 
25 |     if (Array.isArray(result)) {
26 |       throw new Error('Unexpected array result');
27 |     }
28 | 
29 |     expect(result.count).toEqual(42);
30 |   });
31 | 
32 |   it('calls backlog.getNotificationsCount with correct params', async () => {
33 |     const params = {
34 |       alreadyRead: true,
35 |       resourceAlreadyRead: false,
36 |     };
37 | 
38 |     await tool.handler(params);
39 | 
40 |     expect(mockBacklog.getNotificationsCount).toHaveBeenCalledWith(params);
41 |   });
42 | });
43 | 
```

--------------------------------------------------------------------------------
/src/tools/getSpace.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getSpaceTool } from './getSpace.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getSpaceTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getSpace: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       spaceKey: 'demo',
10 |       name: 'Demo Space',
11 |       ownerId: 1,
12 |       lang: 'en',
13 |       timezone: 'Asia/Tokyo',
14 |       reportSendTime: '08:00:00',
15 |       textFormattingRule: 'backlog',
16 |       created: '2023-01-01T00:00:00Z',
17 |       updated: '2023-01-01T00:00:00Z',
18 |     }),
19 |   };
20 | 
21 |   const mockTranslationHelper = createTranslationHelper();
22 |   const tool = getSpaceTool(mockBacklog as Backlog, mockTranslationHelper);
23 | 
24 |   it('returns space information as formatted JSON text', async () => {
25 |     const result = await tool.handler({});
26 | 
27 |     if (Array.isArray(result)) {
28 |       throw new Error('Unexpected array result');
29 |     }
30 |     expect(result.name).toEqual('Demo Space');
31 |     expect(result.spaceKey).toEqual('demo');
32 |   });
33 | 
34 |   it('calls backlog.getSpace', async () => {
35 |     await tool.handler({});
36 | 
37 |     expect(mockBacklog.getSpace).toHaveBeenCalled();
38 |   });
39 | });
40 | 
```

--------------------------------------------------------------------------------
/src/tools/getDocuments.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Backlog } from 'backlog-js';
 2 | import { z } from 'zod';
 3 | import { TranslationHelper } from '../createTranslationHelper.js';
 4 | import { DocumentItemSchema } from '../types/zod/backlogOutputDefinition.js';
 5 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 6 | 
 7 | const getDocumentsSchema = buildToolSchema((t) => ({
 8 |   projectIds: z
 9 |     .array(z.number())
10 |     .describe(t('TOOL_GET_DOCUMENTS_PROJECT_ID_LIST', 'Project ID List')),
11 |   offset: z
12 |     .number()
13 |     .optional()
14 |     .default(0)
15 |     .describe(
16 |       t('TOOL_GET_DOCUMENTS_OFFSET', 'Offset for pagination (default is 0)')
17 |     ),
18 | }));
19 | 
20 | export const getDocumentsTool = (
21 |   backlog: Backlog,
22 |   { t }: TranslationHelper
23 | ): ToolDefinition<
24 |   ReturnType<typeof getDocumentsSchema>,
25 |   (typeof DocumentItemSchema)['shape']
26 | > => {
27 |   return {
28 |     name: 'get_documents',
29 |     description: t(
30 |       'TOOL_GET_DOCUMENTS_DESCRIPTION',
31 |       'Gets a list of documents in a project.'
32 |     ),
33 |     schema: z.object(getDocumentsSchema(t)),
34 |     outputSchema: DocumentItemSchema,
35 |     importantFields: ['id', 'projectId', 'title', 'plain'],
36 |     handler: async ({ projectIds, offset }) => {
37 |       return backlog.getDocuments({ projectId: projectIds, offset });
38 |     },
39 |   };
40 | };
41 | 
```

--------------------------------------------------------------------------------
/src/tools/getPriorities.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getPrioritiesTool } from './getPriorities.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getPrioritiesTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getPriorities: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 2,
11 |         name: 'High',
12 |       },
13 |       {
14 |         id: 3,
15 |         name: 'Normal',
16 |       },
17 |       {
18 |         id: 4,
19 |         name: 'Low',
20 |       },
21 |     ]),
22 |   };
23 | 
24 |   const mockTranslationHelper = createTranslationHelper();
25 |   const tool = getPrioritiesTool(mockBacklog as Backlog, mockTranslationHelper);
26 | 
27 |   it('returns priorities list as formatted JSON text', async () => {
28 |     const result = await tool.handler({});
29 | 
30 |     if (!Array.isArray(result)) {
31 |       throw new Error('Unexpected non array result');
32 |     }
33 | 
34 |     expect(result).toHaveLength(3);
35 |     expect(result[0].name).toContain('High');
36 |     expect(result[1].name).toContain('Normal');
37 |     expect(result[2].name).toContain('Low');
38 |   });
39 | 
40 |   it('calls backlog.getPriorities', async () => {
41 |     await tool.handler({});
42 | 
43 |     expect(mockBacklog.getPriorities).toHaveBeenCalled();
44 |   });
45 | });
46 | 
```

--------------------------------------------------------------------------------
/src/tools/getNotificationsCount.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { NotificationCountSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getNotificationsCountSchema = buildToolSchema((t) => ({
 8 |   alreadyRead: z
 9 |     .boolean()
10 |     .describe(
11 |       t(
12 |         'TOOL_GET_NOTIFICATIONS_COUNT_ALREADY_READ',
13 |         'Whether to include already read notifications'
14 |       )
15 |     ),
16 |   resourceAlreadyRead: z
17 |     .boolean()
18 |     .describe(
19 |       t(
20 |         'TOOL_GET_NOTIFICATIONS_COUNT_RESOURCE_ALREADY_READ',
21 |         'Whether to include notifications for already read resources'
22 |       )
23 |     ),
24 | }));
25 | 
26 | export const getNotificationsCountTool = (
27 |   backlog: Backlog,
28 |   { t }: TranslationHelper
29 | ): ToolDefinition<
30 |   ReturnType<typeof getNotificationsCountSchema>,
31 |   (typeof NotificationCountSchema)['shape']
32 | > => {
33 |   return {
34 |     name: 'count_notifications',
35 |     description: t(
36 |       'TOOL_COUNT_NOTIFICATIONS_DESCRIPTION',
37 |       'Returns count of notifications'
38 |     ),
39 |     schema: z.object(getNotificationsCountSchema(t)),
40 |     outputSchema: NotificationCountSchema,
41 |     handler: async (params) => backlog.getNotificationsCount(params),
42 |   };
43 | };
44 | 
```

--------------------------------------------------------------------------------
/src/tools/getMyself.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getMyselfTool } from './getMyself.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getMyselfTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getMyself: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: 1,
10 |       userId: 'current_user',
11 |       name: 'Current User',
12 |       roleType: 1,
13 |       lang: 'en',
14 |       mailAddress: '[email protected]',
15 |       lastLoginTime: '2023-01-01T00:00:00Z',
16 |       nulabAccount: {
17 |         nulabId: '12345',
18 |         name: 'Current User',
19 |         uniqueId: 'current_user',
20 |       },
21 |     }),
22 |   };
23 | 
24 |   const mockTranslationHelper = createTranslationHelper();
25 |   const tool = getMyselfTool(mockBacklog as Backlog, mockTranslationHelper);
26 | 
27 |   it('returns current user information as formatted JSON text', async () => {
28 |     const result = await tool.handler({});
29 | 
30 |     if (Array.isArray(result)) {
31 |       throw new Error('Unexpected array result');
32 |     }
33 | 
34 |     expect(result.name).toContain('Current User');
35 |     expect(result.mailAddress).toContain('[email protected]');
36 |   });
37 | 
38 |   it('calls backlog.getMyself', async () => {
39 |     await tool.handler({});
40 | 
41 |     expect(mockBacklog.getMyself).toHaveBeenCalled();
42 |   });
43 | });
44 | 
```

--------------------------------------------------------------------------------
/src/utils/tokenCounter.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { countTokens } from './tokenCounter.js';
 2 | import { describe, it, expect } from '@jest/globals';
 3 | 
 4 | describe('countTokens', () => {
 5 |   it('returns 0 for empty string', () => {
 6 |     expect(countTokens('')).toBe(0);
 7 |   });
 8 | 
 9 |   it('counts simple words', () => {
10 |     expect(countTokens('hello world')).toBe(2);
11 |     expect(countTokens('one two three')).toBe(3);
12 |   });
13 | 
14 |   it('ignores multiple spaces/tabs/newlines', () => {
15 |     expect(countTokens('hello     world')).toBe(2);
16 |     expect(countTokens('hello\tworld')).toBe(2);
17 |     expect(countTokens('hello\nworld')).toBe(2);
18 |     expect(countTokens('hello \n\t world')).toBe(2);
19 |   });
20 | 
21 |   it('counts punctuation as separate tokens', () => {
22 |     expect(countTokens('hello, world!')).toBe(4);
23 |     expect(countTokens('foo(bar)')).toBe(4);
24 |   });
25 | 
26 |   it('handles mixed text', () => {
27 |     const input = "This is great, isn't it?";
28 |     // Tokens: ['This', 'is', 'great', ',', 'isn', "'", 't', 'it', '?']
29 |     expect(countTokens(input)).toBe(9);
30 |   });
31 | 
32 |   it('trims leading/trailing whitespace', () => {
33 |     expect(countTokens('   hello world   ')).toBe(2);
34 |   });
35 | 
36 |   it('counts digits and symbols', () => {
37 |     expect(countTokens('123 + 456 = 579')).toBe(5); // ['123', '+', '456', '=', '579']
38 |   });
39 | 
40 |   it('counts Japanese', () => {
41 |     expect(countTokens('こんにちは')).toBe(5);
42 |   });
43 | });
44 | 
```

--------------------------------------------------------------------------------
/src/tools/getIssue.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { IssueSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getIssueSchema = buildToolSchema((t) => ({
 9 |   issueId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t('TOOL_GET_ISSUE_ISSUE_ID', 'The numeric ID of the issue (e.g., 12345)')
14 |     ),
15 |   issueKey: z
16 |     .string()
17 |     .optional()
18 |     .describe(
19 |       t('TOOL_GET_ISSUE_ISSUE_KEY', "The key of the issue (e.g., 'PROJ-123')")
20 |     ),
21 | }));
22 | 
23 | export const getIssueTool = (
24 |   backlog: Backlog,
25 |   { t }: TranslationHelper
26 | ): ToolDefinition<
27 |   ReturnType<typeof getIssueSchema>,
28 |   (typeof IssueSchema)['shape']
29 | > => {
30 |   return {
31 |     name: 'get_issue',
32 |     description: t(
33 |       'TOOL_GET_ISSUE_DESCRIPTION',
34 |       'Returns information about a specific issue'
35 |     ),
36 |     outputSchema: IssueSchema,
37 |     schema: z.object(getIssueSchema(t)),
38 |     handler: async ({ issueId, issueKey }) => {
39 |       const result = resolveIdOrKey('issue', { id: issueId, key: issueKey }, t);
40 |       if (!result.ok) {
41 |         throw result.error;
42 |       }
43 |       return backlog.getIssue(result.value);
44 |     },
45 |   };
46 | };
47 | 
```

--------------------------------------------------------------------------------
/src/tools/deleteIssue.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { IssueSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const deleteIssueSchema = buildToolSchema((t) => ({
 9 |   issueId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_DELETE_ISSUE_ISSUE_ID',
15 |         'The numeric ID of the issue (e.g., 12345)'
16 |       )
17 |     ),
18 |   issueKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t('TOOL_GET_ISSUE_ISSUE_KEY', "The key of the issue (e.g., 'PROJ-123')")
23 |     ),
24 | }));
25 | 
26 | export const deleteIssueTool = (
27 |   backlog: Backlog,
28 |   { t }: TranslationHelper
29 | ): ToolDefinition<
30 |   ReturnType<typeof deleteIssueSchema>,
31 |   (typeof IssueSchema)['shape']
32 | > => {
33 |   return {
34 |     name: 'delete_issue',
35 |     description: t('TOOL_DELETE_ISSUE_DESCRIPTION', 'Deletes an issue'),
36 |     schema: z.object(deleteIssueSchema(t)),
37 |     outputSchema: IssueSchema,
38 |     handler: async ({ issueId, issueKey }) => {
39 |       const result = resolveIdOrKey('issue', { id: issueId, key: issueKey }, t);
40 |       if (!result.ok) {
41 |         throw result.error;
42 |       }
43 |       return backlog.deleteIssue(result.value);
44 |     },
45 |   };
46 | };
47 | 
```

--------------------------------------------------------------------------------
/src/handlers/transformers/wrapWithErrorHandling.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { wrapWithErrorHandling } from './wrapWithErrorHandling';
 2 | import { isErrorLike, type ErrorLike } from '../../types/result';
 3 | import { describe, it, expect } from '@jest/globals';
 4 | 
 5 | describe('wrapWithErrorHandling', () => {
 6 |   it('returns success result when function resolves', async () => {
 7 |     const fn = async (input: number) => input + 1;
 8 |     const wrapped = wrapWithErrorHandling(fn);
 9 | 
10 |     const result = await wrapped(1);
11 | 
12 |     expect(result).toEqual({ kind: 'ok', data: 2 });
13 |   });
14 | 
15 |   it('returns error result with default handler when function throws', async () => {
16 |     const fn = async () => {
17 |       throw new Error('fail');
18 |     };
19 |     const wrapped = wrapWithErrorHandling(fn);
20 | 
21 |     const result = await wrapped(undefined as never);
22 | 
23 |     expect(result.kind).toBe('error');
24 |     if (isErrorLike(result)) {
25 |       expect(result.message).toMatch(/fail/);
26 |     }
27 |   });
28 | 
29 |   it('uses custom error handler if provided', async () => {
30 |     const fn = async () => {
31 |       throw new Error('original');
32 |     };
33 | 
34 |     const customHandler = (_: unknown): ErrorLike => ({
35 |       kind: 'error',
36 |       message: 'custom error handled',
37 |     });
38 | 
39 |     const wrapped = wrapWithErrorHandling(fn, customHandler);
40 | 
41 |     const result = await wrapped(undefined as never);
42 | 
43 |     expect(result).toEqual({ kind: 'error', message: 'custom error handled' });
44 |   });
45 | });
46 | 
```

--------------------------------------------------------------------------------
/src/tools/addWiki.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { WikiSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const addWikiSchema = buildToolSchema((t) => ({
 8 |   projectId: z.number().describe(t('TOOL_ADD_WIKI_PROJECT_ID', 'Project ID')),
 9 |   name: z.string().describe(t('TOOL_ADD_WIKI_NAME', 'Name of the wiki page')),
10 |   content: z
11 |     .string()
12 |     .describe(t('TOOL_ADD_WIKI_CONTENT', 'Content of the wiki page')),
13 |   mailNotify: z
14 |     .boolean()
15 |     .optional()
16 |     .describe(
17 |       t(
18 |         'TOOL_ADD_WIKI_MAIL_NOTIFY',
19 |         'Whether to send notification emails (default: false)'
20 |       )
21 |     ),
22 | }));
23 | 
24 | export const addWikiTool = (
25 |   backlog: Backlog,
26 |   { t }: TranslationHelper
27 | ): ToolDefinition<
28 |   ReturnType<typeof addWikiSchema>,
29 |   (typeof WikiSchema)['shape']
30 | > => {
31 |   return {
32 |     name: 'add_wiki',
33 |     description: t('TOOL_ADD_WIKI_DESCRIPTION', 'Creates a new wiki page'),
34 |     schema: z.object(addWikiSchema(t)),
35 |     outputSchema: WikiSchema,
36 |     importantFields: ['id', 'name', 'content', 'createdUser'],
37 |     handler: async ({ projectId, name, content, mailNotify }) =>
38 |       backlog.postWiki({
39 |         projectId,
40 |         name,
41 |         content,
42 |         mailNotify,
43 |       }),
44 |   };
45 | };
46 | 
```

--------------------------------------------------------------------------------
/src/tools/getResolutions.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getResolutionsTool } from './getResolutions.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getResolutionsTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getResolutions: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 0,
11 |         name: 'Fixed',
12 |       },
13 |       {
14 |         id: 1,
15 |         name: "Won't Fix",
16 |       },
17 |       {
18 |         id: 2,
19 |         name: 'Invalid',
20 |       },
21 |       {
22 |         id: 3,
23 |         name: 'Duplicate',
24 |       },
25 |     ]),
26 |   };
27 | 
28 |   const mockTranslationHelper = createTranslationHelper();
29 |   const tool = getResolutionsTool(
30 |     mockBacklog as Backlog,
31 |     mockTranslationHelper
32 |   );
33 | 
34 |   it('returns resolutions list as formatted JSON text', async () => {
35 |     const result = await tool.handler({});
36 | 
37 |     if (!Array.isArray(result)) {
38 |       throw new Error('Unexpected non array result');
39 |     }
40 |     expect(result).toHaveLength(4);
41 |     expect(result[0].name).toContain('Fixed');
42 |     expect(result[1].name).toContain("Won't Fix");
43 |     expect(result[2].name).toContain('Invalid');
44 |     expect(result[3].name).toContain('Duplicate');
45 |   });
46 | 
47 |   it('calls backlog.getResolutions', async () => {
48 |     await tool.handler({});
49 | 
50 |     expect(mockBacklog.getResolutions).toHaveBeenCalled();
51 |   });
52 | });
53 | 
```

--------------------------------------------------------------------------------
/src/tools/deleteProject.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { ProjectSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const deleteProjectSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_DELETE_PROJECT_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_DELETE_PROJECT_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 | }));
28 | 
29 | export const deleteProjectTool = (
30 |   backlog: Backlog,
31 |   { t }: TranslationHelper
32 | ): ToolDefinition<
33 |   ReturnType<typeof deleteProjectSchema>,
34 |   (typeof ProjectSchema)['shape']
35 | > => {
36 |   return {
37 |     name: 'delete_project',
38 |     description: t('TOOL_DELETE_PROJECT_DESCRIPTION', 'Deletes a project'),
39 |     schema: z.object(deleteProjectSchema(t)),
40 |     outputSchema: ProjectSchema,
41 |     handler: async ({ projectId, projectKey }) => {
42 |       const result = resolveIdOrKey(
43 |         'project',
44 |         { id: projectId, key: projectKey },
45 |         t
46 |       );
47 |       if (!result.ok) {
48 |         throw result.error;
49 |       }
50 |       return backlog.deleteProject(result.value);
51 |     },
52 |   };
53 | };
54 | 
```

--------------------------------------------------------------------------------
/src/tools/getUsers.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getUsersTool } from './getUsers.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getUsersTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getUsers: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 1,
11 |         userId: 'admin',
12 |         name: 'Admin User',
13 |         roleType: 1,
14 |         lang: 'en',
15 |         mailAddress: '[email protected]',
16 |         lastLoginTime: '2023-01-01T00:00:00Z',
17 |       },
18 |       {
19 |         id: 2,
20 |         userId: 'user1',
21 |         name: 'Regular User',
22 |         roleType: 2,
23 |         lang: 'en',
24 |         mailAddress: '[email protected]',
25 |         lastLoginTime: '2023-01-02T00:00:00Z',
26 |       },
27 |     ]),
28 |   };
29 | 
30 |   const mockTranslationHelper = createTranslationHelper();
31 |   const tool = getUsersTool(mockBacklog as Backlog, mockTranslationHelper);
32 | 
33 |   it('returns users list as formatted JSON text', async () => {
34 |     const result = await tool.handler({});
35 | 
36 |     if (!Array.isArray(result)) {
37 |       throw new Error('Unexpected array result');
38 |     }
39 |     expect(result).toHaveLength(2);
40 |     expect(result[0].name).toContain('Admin User');
41 |     expect(result[1].name).toContain('Regular User');
42 |   });
43 | 
44 |   it('calls backlog.getUsers', async () => {
45 |     await tool.handler({});
46 | 
47 |     expect(mockBacklog.getUsers).toHaveBeenCalled();
48 |   });
49 | });
50 | 
```

--------------------------------------------------------------------------------
/src/tools/getNotifications.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { NotificationSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getNotificationsSchema = buildToolSchema((t) => ({
 8 |   minId: z
 9 |     .number()
10 |     .optional()
11 |     .describe(t('TOOL_GET_NOTIFICATIONS_MIN_ID', 'Minimum notification ID')),
12 |   maxId: z
13 |     .number()
14 |     .optional()
15 |     .describe(t('TOOL_GET_NOTIFICATIONS_MAX_ID', 'Maximum notification ID')),
16 |   count: z
17 |     .number()
18 |     .optional()
19 |     .describe(
20 |       t('TOOL_GET_NOTIFICATIONS_COUNT', 'Number of notifications to retrieve')
21 |     ),
22 |   order: z
23 |     .enum(['asc', 'desc'])
24 |     .optional()
25 |     .describe(t('TOOL_GET_NOTIFICATIONS_ORDER', 'Sort order')),
26 | }));
27 | 
28 | export const getNotificationsTool = (
29 |   backlog: Backlog,
30 |   { t }: TranslationHelper
31 | ): ToolDefinition<
32 |   ReturnType<typeof getNotificationsSchema>,
33 |   (typeof NotificationSchema)['shape']
34 | > => {
35 |   return {
36 |     name: 'get_notifications',
37 |     description: t(
38 |       'TOOL_GET_NOTIFICATIONS_DESCRIPTION',
39 |       'Returns list of notifications'
40 |     ),
41 |     schema: z.object(getNotificationsSchema(t)),
42 |     outputSchema: NotificationSchema,
43 |     handler: async ({ minId, maxId, count, order }) =>
44 |       backlog.getNotifications({
45 |         minId,
46 |         maxId,
47 |         count,
48 |         order,
49 |       }),
50 |   };
51 | };
52 | 
```

--------------------------------------------------------------------------------
/src/tools/getProject.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { ProjectSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getProjectSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_PROJECT_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_PROJECT_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 | }));
28 | 
29 | export const getProjectTool = (
30 |   backlog: Backlog,
31 |   { t }: TranslationHelper
32 | ): ToolDefinition<
33 |   ReturnType<typeof getProjectSchema>,
34 |   (typeof ProjectSchema)['shape']
35 | > => {
36 |   return {
37 |     name: 'get_project',
38 |     description: t(
39 |       'TOOL_GET_PROJECT_DESCRIPTION',
40 |       'Returns information about a specific project'
41 |     ),
42 |     schema: z.object(getProjectSchema(t)),
43 |     outputSchema: ProjectSchema,
44 |     handler: async ({ projectId, projectKey }) => {
45 |       const result = resolveIdOrKey(
46 |         'project',
47 |         { id: projectId, key: projectKey },
48 |         t
49 |       );
50 |       if (!result.ok) {
51 |         throw result.error;
52 |       }
53 |       return backlog.getProject(result.value);
54 |     },
55 |   };
56 | };
57 | 
```

--------------------------------------------------------------------------------
/src/tools/getProjectList.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { ProjectSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const getProjectListSchema = buildToolSchema((t) => ({
 8 |   archived: z
 9 |     .boolean()
10 |     .optional()
11 |     .describe(
12 |       t(
13 |         'TOOL_GET_PROJECT_LIST_ARCHIVED',
14 |         'For unspecified parameters, this form returns all projects. For ‘false’ parameters, it returns unarchived projects. For ‘true’ parameters, it returns archived projects.'
15 |       )
16 |     ),
17 |   all: z
18 |     .boolean()
19 |     .optional()
20 |     .describe(
21 |       t(
22 |         'TOOL_GET_PROJECT_LIST_ALL',
23 |         'Only applies to administrators. If ‘true,’ it returns all projects. If ‘false,’ it returns only projects they have joined.'
24 |       )
25 |     ),
26 | }));
27 | 
28 | export const getProjectListTool = (
29 |   backlog: Backlog,
30 |   { t }: TranslationHelper
31 | ): ToolDefinition<
32 |   ReturnType<typeof getProjectListSchema>,
33 |   (typeof ProjectSchema)['shape']
34 | > => {
35 |   return {
36 |     name: 'get_project_list',
37 |     description: t(
38 |       'TOOL_GET_PROJECT_LIST_DESCRIPTION',
39 |       'Returns list of projects'
40 |     ),
41 |     schema: z.object(getProjectListSchema(t)),
42 |     outputSchema: ProjectSchema,
43 |     importantFields: ['id', 'projectKey', 'name'],
44 |     handler: async ({ archived, all }) =>
45 |       backlog.getProjects({ archived, all }),
46 |   };
47 | };
48 | 
```

--------------------------------------------------------------------------------
/src/utils/runToolSafely.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, expect, it } from '@jest/globals';
 2 | import { ErrorLike, isErrorLike } from '../types/result.js';
 3 | import { runToolSafely } from './runToolSafely.js';
 4 | 
 5 | describe('runToolSafely', () => {
 6 |   it('returns ok result when handler succeeds', async () => {
 7 |     const mockFn = async (input: number) => input * 2;
 8 | 
 9 |     const safeFn = runToolSafely<number, number>(mockFn);
10 | 
11 |     const result = await safeFn(3);
12 | 
13 |     expect(result).toEqual({ kind: 'ok', data: 6 });
14 |   });
15 | 
16 |   it('returns error result when handler throws (default handler)', async () => {
17 |     const mockFn = async () => {
18 |       throw new Error('Boom');
19 |     };
20 | 
21 |     const safeFn = runToolSafely(mockFn);
22 | 
23 |     const result = await safeFn(undefined as never);
24 | 
25 |     expect(result.kind).toBe('error');
26 |     if (isErrorLike(result)) {
27 |       expect(result.message).toMatch(/Boom/);
28 |     } else {
29 |       throw new Error('Expected error result, but got success');
30 |     }
31 |   });
32 | 
33 |   it('uses custom error handler when provided', async () => {
34 |     const mockFn = async () => {
35 |       throw new Error('Something went wrong');
36 |     };
37 | 
38 |     const customErrorHandler = (err: unknown): ErrorLike => ({
39 |       kind: 'error',
40 |       message: 'Custom: ' + (err as Error).message,
41 |     });
42 | 
43 |     const safeFn = runToolSafely(mockFn, customErrorHandler);
44 | 
45 |     const result = await safeFn(undefined as never);
46 | 
47 |     expect(result).toEqual({
48 |       kind: 'error',
49 |       message: 'Custom: Something went wrong',
50 |     });
51 |   });
52 | });
53 | 
```

--------------------------------------------------------------------------------
/src/tools/getWikisCount.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { WikiCountSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getWikisCountSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_WIKIS_COUNT_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_WIKIS_COUNT_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 | }));
28 | 
29 | export const getWikisCountTool = (
30 |   backlog: Backlog,
31 |   { t }: TranslationHelper
32 | ): ToolDefinition<
33 |   ReturnType<typeof getWikisCountSchema>,
34 |   (typeof WikiCountSchema)['shape']
35 | > => {
36 |   return {
37 |     name: 'get_wikis_count',
38 |     description: t(
39 |       'TOOL_GET_WIKIS_COUNT_DESCRIPTION',
40 |       'Returns count of wiki pages in a project'
41 |     ),
42 |     schema: z.object(getWikisCountSchema(t)),
43 |     outputSchema: WikiCountSchema,
44 |     handler: async ({ projectId, projectKey }) => {
45 |       const result = resolveIdOrKey(
46 |         'project',
47 |         { id: projectId, key: projectKey },
48 |         t
49 |       );
50 |       if (!result.ok) {
51 |         throw result.error;
52 |       }
53 |       return backlog.getWikisCount(result.value);
54 |     },
55 |   };
56 | };
57 | 
```

--------------------------------------------------------------------------------
/src/tools/getWikisCount.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getWikisCountTool } from './getWikisCount.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getWikisCountTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getWikisCount: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       count: 42,
10 |     }),
11 |   };
12 | 
13 |   const mockTranslationHelper = createTranslationHelper();
14 |   const tool = getWikisCountTool(mockBacklog as Backlog, mockTranslationHelper);
15 | 
16 |   it('returns wiki count as formatted JSON text', async () => {
17 |     const result = await tool.handler({
18 |       projectKey: 'TEST',
19 |     });
20 | 
21 |     if (Array.isArray(result)) {
22 |       throw new Error('Unexpected array result');
23 |     }
24 |     expect(result.count).toEqual(42);
25 |   });
26 | 
27 |   it('calls backlog.getWikisCount with correct params when using project key', async () => {
28 |     await tool.handler({
29 |       projectKey: 'TEST',
30 |     });
31 | 
32 |     expect(mockBacklog.getWikisCount).toHaveBeenCalledWith('TEST');
33 |   });
34 | 
35 |   it('calls backlog.getWikisCount with correct params when using project ID', async () => {
36 |     await tool.handler({
37 |       projectId: 100,
38 |     });
39 | 
40 |     expect(mockBacklog.getWikisCount).toHaveBeenCalledWith(100);
41 |   });
42 | 
43 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
44 |     const params = {}; // No identifier provided
45 | 
46 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
47 |   });
48 | });
49 | 
```

--------------------------------------------------------------------------------
/memory-bank/projectbrief.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Project Overview
 2 | 
 3 | ## Purpose
 4 | - Build an MCP server to connect with the Backlog API
 5 | - Use backlog-js for connecting to Backlog
 6 | - The BacklogJS interface is published [here](https://github.com/nulab/backlog-js/blob/master/src/backlog.ts)
 7 | 
 8 | ## Implementation Approach
 9 | - Create tools corresponding to each API endpoint and place them in `./src/tools/${endpointName}.ts`
10 | - Write endpoint names in camelCase (e.g., `getProjectList`)
11 | - Create corresponding test files (`${endpointName}.test.ts`) for each tool
12 | - Refer to the API endpoints listed in URLlist.md for implementation
13 | 
14 | ## Basic Tool Structure
15 | 1. Tool Definition
16 |    - Name: Name representing the API endpoint (e.g., `get_space`)
17 |    - Description: Description of the tool's functionality (in English)
18 |    - Schema: Definition of input parameters (using Zod)
19 |    - Handler: Function that performs the actual processing
20 | 
21 | 2. Internationalization
22 |    - Descriptions are defined in a translatable format
23 |    - Descriptions can be customized via the `.backlog-mcp-serverrc.json` file
24 | 
25 | 3. Testing
26 |    - Create test files corresponding to each tool
27 |    - Use mocks to simulate Backlog API calls
28 | 
29 | ## Deployment Method
30 | - Provided as a Docker container
31 | - Published to GitHub Container Registry (ghcr.io)
32 | - Configuration injected via environment variables (`BACKLOG_DOMAIN`, `BACKLOG_API_KEY`)
33 | 
34 | ## Usage
35 | - Register as an MCP server in Claude settings
36 | - Set necessary environment variables when running Docker
37 | - Multi-language support available through translation files
38 | 
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Release 
 2 | 
 3 | on:
 4 |   workflow_dispatch:
 5 |     inputs:
 6 |       version:
 7 |         description: 'Release version (e.g. 2.3.0). Leave empty for auto.'
 8 |         required: false
 9 | permissions:
10 |   contents: write
11 |   packages: write
12 | jobs:
13 |   release:
14 |     runs-on: ubuntu-latest
15 | 
16 |     steps:
17 |       - name: Checkout code
18 |         uses: actions/checkout@v3
19 | 
20 |       - name: Set Git user
21 |         run: |
22 |           git config --global user.name "github-actions[bot]"
23 |           git config --global user.email "github-actions[bot]@users.noreply.github.com"
24 | 
25 |       - name: Setup Node.js
26 |         uses: actions/setup-node@v4
27 |         with:
28 |           node-version: 22
29 | 
30 |       - name: Install dependencies
31 |         run: npm ci
32 | 
33 |       - name: Set up QEMU for cross-platform builds
34 |         uses: docker/setup-qemu-action@v3
35 | 
36 |       - name: Set up Docker Buildx
37 |         uses: docker/setup-buildx-action@v3
38 | 
39 |       - name: Login to GitHub Container Registry (ghcr.io)
40 |         uses: docker/login-action@v3
41 |         with:
42 |           registry: ghcr.io
43 |           username: ${{ github.actor }}
44 |           password: ${{ secrets.GITHUB_TOKEN }}
45 | 
46 |       - name: Run release-it
47 |         run: |
48 |           if [ -n "${{ github.event.inputs.version }}" ]; then
49 |             echo "Manual version input: ${{ github.event.inputs.version }}"
50 |             npx release-it ${{ github.event.inputs.version }} -y --ci
51 |           else
52 |             echo "Auto version release"
53 |             npx release-it -y --ci
54 |           fi
55 |         env:
56 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57 | 
```

--------------------------------------------------------------------------------
/src/tools/getCategories.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { CategorySchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getCategoriesSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_CATEGORIES_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_CATEGORIES_PROJECT_ID',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 | }));
28 | 
29 | export const getCategoriesTool = (
30 |   backlog: Backlog,
31 |   { t }: TranslationHelper
32 | ): ToolDefinition<
33 |   ReturnType<typeof getCategoriesSchema>,
34 |   (typeof CategorySchema)['shape']
35 | > => {
36 |   return {
37 |     name: 'get_categories',
38 |     description: t(
39 |       'TOOL_GET_CATEGORIES_DESCRIPTION',
40 |       'Returns list of categories for a project'
41 |     ),
42 |     schema: z.object(getCategoriesSchema(t)),
43 |     importantFields: ['id', 'projectId', 'name'],
44 |     outputSchema: CategorySchema,
45 |     handler: async ({ projectId, projectKey }) => {
46 |       const result = resolveIdOrKey(
47 |         'project',
48 |         { id: projectId, key: projectKey },
49 |         t
50 |       );
51 |       if (!result.ok) {
52 |         throw result.error;
53 |       }
54 |       return backlog.getCategories(result.value);
55 |     },
56 |   };
57 | };
58 | 
```

--------------------------------------------------------------------------------
/src/tools/getIssueTypes.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { IssueTypeSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getIssueTypesSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_GIT_REPOSITORIES_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_GIT_REPOSITORIES_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 | }));
28 | 
29 | export const getIssueTypesTool = (
30 |   backlog: Backlog,
31 |   { t }: TranslationHelper
32 | ): ToolDefinition<
33 |   ReturnType<typeof getIssueTypesSchema>,
34 |   (typeof IssueTypeSchema)['shape']
35 | > => {
36 |   return {
37 |     name: 'get_issue_types',
38 |     description: t(
39 |       'TOOL_GET_ISSUE_TYPES_DESCRIPTION',
40 |       'Returns list of issue types for a project'
41 |     ),
42 |     schema: z.object(getIssueTypesSchema(t)),
43 |     outputSchema: IssueTypeSchema,
44 |     importantFields: ['id', 'name'],
45 |     handler: async ({ projectId, projectKey }) => {
46 |       const result = resolveIdOrKey(
47 |         'project',
48 |         { id: projectId, key: projectKey },
49 |         t
50 |       );
51 |       if (!result.ok) {
52 |         throw result.error;
53 |       }
54 |       return backlog.getIssueTypes(result.value);
55 |     },
56 |   };
57 | };
58 | 
```

--------------------------------------------------------------------------------
/src/tools/getGitRepositories.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { GitRepositorySchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getGitRepositoriesSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_GIT_REPOSITORIES_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_GIT_REPOSITORIES_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 | }));
28 | 
29 | export const getGitRepositoriesTool = (
30 |   backlog: Backlog,
31 |   { t }: TranslationHelper
32 | ): ToolDefinition<
33 |   ReturnType<typeof getGitRepositoriesSchema>,
34 |   (typeof GitRepositorySchema)['shape']
35 | > => {
36 |   return {
37 |     name: 'get_git_repositories',
38 |     description: t(
39 |       'TOOL_GET_GIT_REPOSITORIES_DESCRIPTION',
40 |       'Returns list of Git repositories for a project'
41 |     ),
42 |     schema: z.object(getGitRepositoriesSchema(t)),
43 |     outputSchema: GitRepositorySchema,
44 |     handler: async ({ projectId, projectKey }) => {
45 |       const result = resolveIdOrKey(
46 |         'project',
47 |         { id: projectId, key: projectKey },
48 |         t
49 |       );
50 |       if (!result.ok) {
51 |         throw result.error;
52 |       }
53 |       return backlog.getGitRepositories(result.value);
54 |     },
55 |   };
56 | };
57 | 
```

--------------------------------------------------------------------------------
/src/utils/generateFieldsDescription.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { generateFieldsDescription } from './generateFieldsDescription';
 3 | import { describe, it, expect } from '@jest/globals';
 4 | 
 5 | describe('generateFieldsDescription', () => {
 6 |   const schema = z.object({
 7 |     id: z.number(),
 8 |     name: z.string(),
 9 |     active: z.boolean(),
10 |     nested: z
11 |       .object({
12 |         foo: z.string(),
13 |         bar: z.number(),
14 |       })
15 |       .optional(),
16 |   });
17 | 
18 |   it('should generate correct GraphQL description with importantFields', () => {
19 |     const desc = generateFieldsDescription(schema, []);
20 | 
21 |     expect(desc).toContain('Example (query):');
22 |     expect(desc).toContain('id');
23 |     expect(desc).toContain('name');
24 | 
25 |     expect(desc).toContain('type Output {');
26 |     expect(desc).toContain('id: Int!');
27 |     expect(desc).toContain('name: String!');
28 |     expect(desc).toContain('active: Boolean!');
29 |     expect(desc).toContain('nested: JSON');
30 |   });
31 | 
32 |   it('should include all fields in SDL even if not in importantFields', () => {
33 |     const desc = generateFieldsDescription(schema, ['id']);
34 | 
35 |     expect(desc).toContain('id');
36 |     expect(desc).toContain('name: String!');
37 |     expect(desc).toContain('active: Boolean!');
38 |     expect(desc).toContain('nested: JSON');
39 |   });
40 | 
41 |   it('should not duplicate fields in SDL and example', () => {
42 |     const desc = generateFieldsDescription(schema, ['id', 'name']);
43 | 
44 |     const examplePart = desc.split('Output schema')[0];
45 |     expect(examplePart).toContain('id');
46 |     expect(examplePart).toContain('name');
47 |     expect(examplePart).not.toContain('active');
48 |   });
49 | });
50 | 
```

--------------------------------------------------------------------------------
/src/handlers/transformers/wrapWithToolResult.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { wrapWithToolResult } from './wrapWithToolResult.js';
 2 | import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
 3 | import { describe, it, expect } from '@jest/globals';
 4 | 
 5 | describe('wrapWithToolResult', () => {
 6 |   const dummyExtra = {} as RequestHandlerExtra;
 7 | 
 8 |   it('returns error result when SafeResult is error', async () => {
 9 |     const fn = async () =>
10 |       ({ kind: 'error', message: 'Something went wrong' }) as const;
11 |     const wrapped = wrapWithToolResult(fn);
12 | 
13 |     const result = await wrapped({}, dummyExtra);
14 |     expect(result).toEqual({
15 |       isError: true,
16 |       content: [
17 |         {
18 |           type: 'text',
19 |           text: 'Something went wrong',
20 |         },
21 |       ],
22 |     });
23 |   });
24 | 
25 |   it('returns plain text when result data is string', async () => {
26 |     const fn = async () => ({ kind: 'ok', data: 'Hello, world' }) as const;
27 |     const wrapped = wrapWithToolResult(fn);
28 | 
29 |     const result = await wrapped({}, dummyExtra);
30 |     expect(result).toEqual({
31 |       content: [
32 |         {
33 |           type: 'text',
34 |           text: 'Hello, world',
35 |         },
36 |       ],
37 |     });
38 |   });
39 | 
40 |   it('returns JSON text when result data is object', async () => {
41 |     const fn = async () =>
42 |       ({ kind: 'ok', data: { id: 1, name: 'Test' } }) as const;
43 |     const wrapped = wrapWithToolResult(fn);
44 | 
45 |     const result = await wrapped({}, dummyExtra);
46 |     expect(result).toEqual({
47 |       content: [
48 |         {
49 |           type: 'text',
50 |           text: JSON.stringify({ id: 1, name: 'Test' }, null, 2),
51 |         },
52 |       ],
53 |     });
54 |   });
55 | });
56 | 
```

--------------------------------------------------------------------------------
/src/handlers/transformers/wrapWithTokenLimit.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { wrapWithTokenLimit } from './wrapWithTokenLimit.js';
 2 | import { describe, it, expect } from '@jest/globals';
 3 | import { SafeResult } from '../../types/result.js';
 4 | 
 5 | describe('wrapWithTokenLimit', () => {
 6 |   it('returns full JSON string if under maxTokens', async () => {
 7 |     const obj = { id: 1, name: 'Short' };
 8 | 
 9 |     const handler = async () =>
10 |       ({ kind: 'ok', data: obj }) satisfies SafeResult<typeof obj>;
11 | 
12 |     const wrapped = wrapWithTokenLimit(handler, 1000); // 十分余裕あり
13 | 
14 |     const result = await wrapped({});
15 | 
16 |     expect(result.kind).toBe('ok');
17 |     if (result.kind === 'ok') {
18 |       expect(result.data).toBe(JSON.stringify(obj, null, 2));
19 |     }
20 |   });
21 | 
22 |   it('streams and truncates if over maxTokens', async () => {
23 |     const obj = {
24 |       description: 'A '.repeat(5000), // 長文でトークン制限に引っかかる
25 |     };
26 | 
27 |     const handler = async () =>
28 |       ({ kind: 'ok', data: obj }) satisfies SafeResult<typeof obj>;
29 | 
30 |     const wrapped = wrapWithTokenLimit(handler, 100); // 小さな上限
31 | 
32 |     const result = await wrapped({});
33 | 
34 |     expect(result.kind).toBe('ok');
35 |     if (result.kind === 'ok') {
36 |       expect(result.data.length).toBeLessThanOrEqual(500); // 字数でざっくり
37 |       expect(result.data).toMatch(/truncated/i); // デフォルトの切り詰めメッセージが含まれるはず
38 |     }
39 |   });
40 | 
41 |   it('passes through error result unchanged', async () => {
42 |     const handler = async () =>
43 |       ({ kind: 'error', message: 'Boom' }) satisfies SafeResult<unknown>;
44 | 
45 |     const wrapped = wrapWithTokenLimit(handler, 1000);
46 | 
47 |     const result = await wrapped({});
48 | 
49 |     expect(result).toEqual({ kind: 'error', message: 'Boom' });
50 |   });
51 | });
52 | 
```

--------------------------------------------------------------------------------
/src/handlers/builders/composeToolHandler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /* eslint-disable @typescript-eslint/no-explicit-any */
 2 | import { wrapWithErrorHandling } from '../transformers/wrapWithErrorHandling.js';
 3 | import { wrapWithFieldPicking } from '../transformers/wrapWithFieldPicking.js';
 4 | import { wrapWithTokenLimit } from '../transformers/wrapWithTokenLimit.js';
 5 | import { wrapWithToolResult } from '../transformers/wrapWithToolResult.js';
 6 | import { z } from 'zod';
 7 | import { generateFieldsDescription } from '../../utils/generateFieldsDescription.js';
 8 | import { ErrorLike } from '../../types/result.js';
 9 | import { ToolDefinition } from '../../types/tool.js';
10 | 
11 | interface ComposeOptions {
12 |   useFields: boolean;
13 |   errorHandler?: (err: unknown) => ErrorLike;
14 |   maxTokens: number;
15 | }
16 | 
17 | export function composeToolHandler(
18 |   tool: ToolDefinition<any, any>,
19 |   options: ComposeOptions
20 | ) {
21 |   const { useFields, errorHandler, maxTokens } = options;
22 | 
23 |   // Step 1: Add `fields` to schema if needed
24 |   if (useFields) {
25 |     const fieldDesc = generateFieldsDescription(
26 |       tool.outputSchema,
27 |       (tool.importantFields as string[]) ?? [],
28 |       tool.name
29 |     );
30 |     tool.schema = extendSchema(tool.schema, fieldDesc);
31 |   }
32 | 
33 |   // Step 2: Compose
34 |   let handler = wrapWithErrorHandling(tool.handler, errorHandler);
35 | 
36 |   if (useFields) {
37 |     handler = wrapWithFieldPicking(handler);
38 |   }
39 | 
40 |   return wrapWithToolResult(wrapWithTokenLimit(handler, maxTokens));
41 | }
42 | 
43 | function extendSchema<I extends z.ZodRawShape>(
44 |   schema: z.ZodObject<I>,
45 |   desc: string
46 | ): z.ZodObject<I & { fields: z.ZodString }> {
47 |   return schema.extend({
48 |     fields: z.string().describe(desc),
49 |   }) as z.ZodObject<I & { fields: z.ZodString }>;
50 | }
51 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "backlog-mcp-server",
 3 |   "version": "0.4.0",
 4 |   "type": "module",
 5 |   "bin": {
 6 |     "backlog-mcp-server": "./build/index.js"
 7 |   },
 8 |   "license": "MIT",
 9 |   "scripts": {
10 |     "dev": "NODE_ENV=development node --loader ts-node/esm src/index.ts",
11 |     "prebuild": "node scripts/replace-version.js",
12 |     "build": "tsc && chmod 755 build/index.js",
13 |     "test": "NODE_OPTIONS=--experimental-vm-modules jest",
14 |     "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
15 |     "lint": "eslint . --ext .ts",
16 |     "lint:fix": "eslint . --ext .ts --fix",
17 |     "format": "prettier --check \"**/*.{ts,tsx}\"",
18 |     "format:fix": "prettier --write \"**/*.{ts,tsx}\""
19 |   },
20 |   "files": [
21 |     "build"
22 |   ],
23 |   "dependencies": {
24 |     "@modelcontextprotocol/sdk": "^1.9.0",
25 |     "backlog-js": "^0.13.6",
26 |     "cosmiconfig": "^9.0.0",
27 |     "dotenv": "^16.5.0",
28 |     "env-var": "^7.5.0",
29 |     "graphql": "^16.11.0",
30 |     "node-fetch": "^3.3.2",
31 |     "pino": "^9.9.0",
32 |     "pino-pretty": "^13.1.1",
33 |     "yargs": "^18.0.0",
34 |     "zod": "^3.24.3"
35 |   },
36 |   "devDependencies": {
37 |     "@eslint/js": "^9.24.0",
38 |     "@release-it/conventional-changelog": "^10.0.1",
39 |     "@types/jest": "^29.5.14",
40 |     "@types/node": "^22.14.1",
41 |     "@types/yargs": "^17.0.33",
42 |     "@typescript-eslint/eslint-plugin": "^8.30.1",
43 |     "@typescript-eslint/parser": "^8.30.1",
44 |     "@typescript-eslint/utils": "^8.30.1",
45 |     "eslint": "^9.24.0",
46 |     "eslint-config-prettier": "^10.1.2",
47 |     "eslint-plugin-prettier": "^5.2.6",
48 |     "jest": "^29.7.0",
49 |     "prettier": "^3.5.3",
50 |     "release-it": "^19.0.0",
51 |     "ts-jest": "^29.3.2",
52 |     "ts-node": "^10.9.2",
53 |     "typescript": "^5.8.3"
54 |   }
55 | }
56 | 
```

--------------------------------------------------------------------------------
/src/backlog/parseBacklogAPIError.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Converts a BacklogError (or unknown error) into Output format for MCP response
 3 |  */
 4 | type MaybeBacklogErrorObject = {
 5 |   _name?: string;
 6 |   _status?: number;
 7 |   _url?: string;
 8 |   _body?: {
 9 |     errors?: {
10 |       message?: string;
11 |       code?: number;
12 |       moreInfo?: string;
13 |     }[];
14 |   };
15 | };
16 | 
17 | export type ParsedBacklogAPIError = {
18 |   type:
19 |     | 'BacklogAuthError'
20 |     | 'BacklogApiError'
21 |     | 'UnexpectedError'
22 |     | 'UnknownError';
23 |   message: string;
24 |   status?: number;
25 |   code?: number;
26 |   url?: string;
27 | };
28 | 
29 | export function parseBacklogAPIError(err: unknown): ParsedBacklogAPIError {
30 |   const e = err as MaybeBacklogErrorObject;
31 | 
32 |   if (e._name && e._status && e._url) {
33 |     const status = e._status;
34 |     const url = e._url;
35 |     const code = e._body?.errors?.[0]?.code;
36 |     const message =
37 |       e._body?.errors?.[0]?.message ?? 'An unknown error occurred.';
38 | 
39 |     if (e._name === 'BacklogAuthError') {
40 |       return {
41 |         type: 'BacklogAuthError',
42 |         message: `Authentication failed (HTTP ${status}). Please check your API key or permissions.`,
43 |         status,
44 |         url,
45 |       };
46 |     }
47 | 
48 |     if (e._name === 'BacklogApiError') {
49 |       return {
50 |         type: 'BacklogApiError',
51 |         message: `Backlog API error (code: ${code}, status: ${status})\n${message}`,
52 |         status,
53 |         code,
54 |         url,
55 |       };
56 |     }
57 | 
58 |     if (e._name === 'UnexpectedError') {
59 |       return {
60 |         type: 'UnexpectedError',
61 |         message: `Unexpected error (HTTP ${status}) while accessing ${url}.`,
62 |         status,
63 |         url,
64 |       };
65 |     }
66 |   }
67 | 
68 |   return {
69 |     type: 'UnknownError',
70 |     message: (err as Error)?.message ?? 'An unknown error occurred.',
71 |   };
72 | }
73 | 
```

--------------------------------------------------------------------------------
/src/utils/generateFieldsDescription.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z, ZodRawShape, ZodTypeAny } from 'zod';
 2 | 
 3 | /**
 4 |  * Generate GraphQL like fields and type specs from Zod types
 5 |  */
 6 | export function generateFieldsDescription(
 7 |   outputSchema: z.ZodObject<ZodRawShape>,
 8 |   importantFields: string[] = [],
 9 |   typeName = 'Output'
10 | ): string {
11 |   const allFields = Object.keys(outputSchema.shape);
12 | 
13 |   // Generate Example Query
14 |   const exampleQueryFields =
15 |     importantFields.length > 0 ? importantFields : allFields;
16 | 
17 |   // Generate Output Schema
18 |   const gqlTypeDef = generateGraphQLType(typeName, outputSchema);
19 | 
20 |   return `
21 | Specify the fields to retrieve using GraphQL query syntax.
22 | Example (query):
23 | {
24 |   ${exampleQueryFields.join('\n  ')}
25 | }
26 | Output schema (type definition):
27 | ${gqlTypeDef}
28 |   `.trim();
29 | }
30 | 
31 | function generateGraphQLType(
32 |   typeName: string,
33 |   schema: z.ZodObject<ZodRawShape>
34 | ): string {
35 |   const lines: string[] = [`type ${typeName} {`];
36 |   for (const [key, value] of Object.entries(schema.shape)) {
37 |     lines.push(`  ${key}: ${mapZodTypeToGraphQLType(value as ZodTypeAny)}`);
38 |   }
39 |   lines.push('}');
40 |   return lines.join('\n');
41 | }
42 | 
43 | /**
44 |  * Zod to graphql
45 |  */
46 | function mapZodTypeToGraphQLType(zodType: z.ZodTypeAny): string {
47 |   if (zodType instanceof z.ZodString) return 'String!';
48 |   if (zodType instanceof z.ZodNumber) return 'Int!';
49 |   if (zodType instanceof z.ZodBoolean) return 'Boolean!';
50 |   if (zodType instanceof z.ZodNullable)
51 |     return mapZodTypeToGraphQLType(zodType.unwrap()).replace(/!$/, '');
52 |   if (zodType instanceof z.ZodOptional)
53 |     return mapZodTypeToGraphQLType(zodType.unwrap()).replace(/!$/, '');
54 | 
55 |   // Spec: a nested part is JSON
56 |   if (zodType instanceof z.ZodObject) return 'JSON';
57 | 
58 |   return 'String';
59 | }
60 | 
```

--------------------------------------------------------------------------------
/src/tools/deleteVersion.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { deleteVersionTool } from './deleteVersion.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('deleteVersionTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     deleteVersions: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: 1,
10 |       projectId: 100,
11 |       name: 'Test Version',
12 |       description: '',
13 |       startDate: null,
14 |       releaseDueDate: null,
15 |       archived: false,
16 |       displayOrder: 0,
17 |     }),
18 |   };
19 | 
20 |   const mockTranslationHelper = createTranslationHelper();
21 |   const tool = deleteVersionTool(mockBacklog as Backlog, mockTranslationHelper);
22 | 
23 |   it('returns deleted version information', async () => {
24 |     const result = await tool.handler({
25 |       projectKey: 'TEST',
26 |       id: 1,
27 |     });
28 | 
29 |     expect(result).toHaveProperty('id', 1);
30 |     expect(result).toHaveProperty('name', 'Test Version');
31 |   });
32 | 
33 |   it('calls backlog.deleteVersions with correct params when using project key', async () => {
34 |     await tool.handler({
35 |       projectKey: 'TEST',
36 |       id: 1,
37 |     });
38 | 
39 |     expect(mockBacklog.deleteVersions).toHaveBeenCalledWith('TEST', 1);
40 |   });
41 | 
42 |   it('calls backlog.deleteVersions with correct params when using project ID', async () => {
43 |     await tool.handler({
44 |       projectId: 100,
45 |       id: 1,
46 |     });
47 | 
48 |     expect(mockBacklog.deleteVersions).toHaveBeenCalledWith(100, 1);
49 |   });
50 | 
51 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
52 |     const params = { id: 1 }; // No identifier provided
53 | 
54 |     await expect(tool.handler(params)).rejects.toThrowError(Error);
55 |   });
56 | });
57 | 
```

--------------------------------------------------------------------------------
/src/tools/getVersionMilestoneList.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getVersionMilestoneListSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_VERSION_MILESTONE_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_VERSION_MILESTONE_PROJECT_KEY',
24 |         'The key of the project (e.g., TEST_PROJECT)'
25 |       )
26 |     ),
27 | }));
28 | 
29 | export const getVersionMilestoneListTool = (
30 |   backlog: Backlog,
31 |   { t }: TranslationHelper
32 | ): ToolDefinition<
33 |   ReturnType<typeof getVersionMilestoneListSchema>,
34 |   (typeof VersionSchema)['shape']
35 | > => {
36 |   return {
37 |     name: 'get_version_milestone_list',
38 |     description: t(
39 |       'TOOL_GET_VERSION_MILESTONE_LIST_DESCRIPTION',
40 |       'Returns list of versions/milestones in the Backlog space'
41 |     ),
42 |     schema: z.object(getVersionMilestoneListSchema(t)),
43 |     outputSchema: VersionSchema,
44 |     importantFields: [
45 |       'id',
46 |       'name',
47 |       'description',
48 |       'startDate',
49 |       'releaseDueDate',
50 |       'archived',
51 |     ],
52 |     handler: async ({ projectId, projectKey }) => {
53 |       const result = resolveIdOrKey(
54 |         'project',
55 |         { id: projectId, key: projectKey },
56 |         t
57 |       );
58 |       if (!result.ok) {
59 |         throw result.error;
60 |       }
61 |       return backlog.getVersions(result.value);
62 |     },
63 |   };
64 | };
65 | 
```

--------------------------------------------------------------------------------
/src/tools/getWikiPages.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { WikiListItemSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getWikiPagesSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_WIKI_PAGES_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_WIKI_PAGES_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 |   keyword: z
28 |     .string()
29 |     .optional()
30 |     .describe(
31 |       t('TOOL_GET_WIKI_PAGES_KEYWORD', 'Keyword to search for in Wiki pages')
32 |     ),
33 | }));
34 | 
35 | export const getWikiPagesTool = (
36 |   backlog: Backlog,
37 |   { t }: TranslationHelper
38 | ): ToolDefinition<
39 |   ReturnType<typeof getWikiPagesSchema>,
40 |   (typeof WikiListItemSchema)['shape']
41 | > => {
42 |   return {
43 |     name: 'get_wiki_pages',
44 |     description: t(
45 |       'TOOL_GET_WIKI_PAGES_DESCRIPTION',
46 |       'Returns list of Wiki pages'
47 |     ),
48 |     schema: z.object(getWikiPagesSchema(t)),
49 |     outputSchema: WikiListItemSchema,
50 |     importantFields: ['projectId', 'name', 'tags'],
51 |     handler: async ({ projectId, projectKey, keyword }) => {
52 |       const result = resolveIdOrKey(
53 |         'project',
54 |         { id: projectId, key: projectKey },
55 |         t
56 |       );
57 |       if (!result.ok) {
58 |         throw result.error;
59 |       }
60 |       return backlog.getWikis({
61 |         projectIdOrKey: result.value,
62 |         keyword,
63 |       });
64 |     },
65 |   };
66 | };
67 | 
```

--------------------------------------------------------------------------------
/src/tools/getWiki.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getWikiTool } from './getWiki.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getWikiTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getWiki: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: 1234,
10 |       projectId: 100,
11 |       name: 'Sample Wiki',
12 |       content: '# Sample Wiki Content\n\nThis is a sample wiki page.',
13 |       tags: [
14 |         { id: 1, name: 'documentation' },
15 |         { id: 2, name: 'guide' },
16 |       ],
17 |       attachments: [],
18 |       sharedFiles: [],
19 |       stars: [],
20 |       createdUser: {
21 |         id: 1,
22 |         userId: 'user1',
23 |         name: 'User One',
24 |       },
25 |       created: '2023-01-01T00:00:00Z',
26 |       updated: '2023-01-02T00:00:00Z',
27 |     }),
28 |   };
29 | 
30 |   const mockTranslationHelper = createTranslationHelper();
31 |   const tool = getWikiTool(mockBacklog as Backlog, mockTranslationHelper);
32 | 
33 |   it('returns wiki information as formatted JSON text', async () => {
34 |     const result = await tool.handler({
35 |       wikiId: 1234,
36 |     });
37 | 
38 |     if (Array.isArray(result)) {
39 |       throw new Error('Unexpected array result');
40 |     }
41 |     expect(result.name).toEqual('Sample Wiki');
42 |     expect(result.content).toContain('Sample Wiki Content');
43 |   });
44 | 
45 |   it('calls backlog.getWiki with correct params when using number ID', async () => {
46 |     await tool.handler({
47 |       wikiId: 1234,
48 |     });
49 | 
50 |     expect(mockBacklog.getWiki).toHaveBeenCalledWith(1234);
51 |   });
52 | 
53 |   it('calls backlog.getWiki with correct params when using string ID', async () => {
54 |     await tool.handler({
55 |       wikiId: '1234',
56 |     });
57 | 
58 |     expect(mockBacklog.getWiki).toHaveBeenCalledWith(1234);
59 |   });
60 | });
61 | 
```

--------------------------------------------------------------------------------
/src/tools/deleteVersion.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const deleteVersionSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_DELETE_VERSION_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_DELETE_VERSION_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 |   id: z
28 |     .number()
29 |     .describe(
30 |       t(
31 |         'TOOL_DELETE_VERSION_ID',
32 |         'The numeric ID of the version to delete (e.g., 67890)'
33 |       )
34 |     ),
35 | }));
36 | 
37 | export const deleteVersionTool = (
38 |   backlog: Backlog,
39 |   { t }: TranslationHelper
40 | ): ToolDefinition<
41 |   ReturnType<typeof deleteVersionSchema>,
42 |   (typeof VersionSchema)['shape']
43 | > => {
44 |   return {
45 |     name: 'delete_version',
46 |     description: t(
47 |       'TOOL_DELETE_VERSION_DESCRIPTION',
48 |       'Deletes a version from a project'
49 |     ),
50 |     schema: z.object(deleteVersionSchema(t)),
51 |     outputSchema: VersionSchema,
52 |     handler: async ({ projectId, projectKey, id }) => {
53 |       const result = resolveIdOrKey(
54 |         'project',
55 |         { id: projectId, key: projectKey },
56 |         t
57 |       );
58 |       if (!result.ok) {
59 |         throw result.error;
60 |       }
61 |       if (!id) {
62 |         throw new Error(
63 |           t('TOOL_DELETE_VERSION_MISSING_ID', 'Version ID is required')
64 |         );
65 |       }
66 |       return backlog.deleteVersions(result.value, id);
67 |     },
68 |   };
69 | };
70 | 
```

--------------------------------------------------------------------------------
/src/tools/getProject.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getProjectTool } from './getProject.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getProjectTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getProject: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: 1,
10 |       projectKey: 'TEST',
11 |       name: 'Test Project',
12 |       chartEnabled: true,
13 |       subtaskingEnabled: true,
14 |       projectLeaderCanEditProjectLeader: false,
15 |       textFormattingRule: 'backlog',
16 |       archived: false,
17 |       displayOrder: 0,
18 |     }),
19 |   };
20 | 
21 |   const mockTranslationHelper = createTranslationHelper();
22 |   const tool = getProjectTool(mockBacklog as Backlog, mockTranslationHelper);
23 | 
24 |   it('returns project information as formatted JSON text', async () => {
25 |     const result = await tool.handler({
26 |       projectKey: 'TEST',
27 |     });
28 | 
29 |     if (Array.isArray(result)) {
30 |       throw new Error('Unexpected array result');
31 |     }
32 |     expect(result.name).toContain('Test Project');
33 |     expect(result.projectKey).toContain('TEST');
34 |   });
35 | 
36 |   it('calls backlog.getProject with correct params when using project key', async () => {
37 |     await tool.handler({
38 |       projectKey: 'TEST',
39 |     });
40 | 
41 |     expect(mockBacklog.getProject).toHaveBeenCalledWith('TEST');
42 |   });
43 | 
44 |   it('calls backlog.getProject with correct params when using project ID', async () => {
45 |     await tool.handler({
46 |       projectId: 1,
47 |     });
48 | 
49 |     expect(mockBacklog.getProject).toHaveBeenCalledWith(1);
50 |   });
51 | 
52 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
53 |     const params = {}; // No identifier provided
54 | 
55 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
56 |   });
57 | });
58 | 
```

--------------------------------------------------------------------------------
/src/tools/getCustomFields.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Backlog } from 'backlog-js';
 2 | import { z } from 'zod';
 3 | import { ToolDefinition, buildToolSchema } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { CustomFieldSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getCustomFieldsInputSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_CUSTOM_FIELDS_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_CUSTOM_FIELDS_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 | }));
28 | 
29 | export const getCustomFieldsTool = (
30 |   backlog: Backlog,
31 |   { t }: TranslationHelper
32 | ): ToolDefinition<
33 |   ReturnType<typeof getCustomFieldsInputSchema>, // Shape for input schema
34 |   (typeof CustomFieldSchema)['shape'] // Shape for output schema (single item)
35 | > => {
36 |   const inputSchemaObject = z.object(getCustomFieldsInputSchema(t)); // Create the ZodObject for input
37 | 
38 |   return {
39 |     name: 'get_custom_fields',
40 |     description: t(
41 |       'TOOL_GET_CUSTOM_FIELDS_DESCRIPTION',
42 |       'Returns list of custom fields for a project'
43 |     ),
44 |     schema: inputSchemaObject,
45 |     outputSchema: CustomFieldSchema,
46 |     importantFields: [
47 |       'id',
48 |       'name',
49 |       'typeId',
50 |       'required',
51 |       'applicableIssueTypes',
52 |     ],
53 |     handler: async ({ projectId, projectKey }) => {
54 |       const result = resolveIdOrKey(
55 |         'project',
56 |         { id: projectId, key: projectKey },
57 |         t
58 |       );
59 |       if (!result.ok) {
60 |         throw result.error;
61 |       }
62 |       return backlog.getCustomFields(result.value);
63 |     },
64 |   };
65 | };
66 | 
```

--------------------------------------------------------------------------------
/src/tools/deleteProject.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { deleteProjectTool } from './deleteProject.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('deleteProjectTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     deleteProject: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: 1,
10 |       projectKey: 'TEST',
11 |       name: 'Test Project',
12 |       chartEnabled: true,
13 |       subtaskingEnabled: true,
14 |       projectLeaderCanEditProjectLeader: false,
15 |       textFormattingRule: 'backlog',
16 |       archived: false,
17 |       displayOrder: 0,
18 |     }),
19 |   };
20 | 
21 |   const mockTranslationHelper = createTranslationHelper();
22 |   const tool = deleteProjectTool(mockBacklog as Backlog, mockTranslationHelper);
23 | 
24 |   it('returns deleted project information', async () => {
25 |     const result = await tool.handler({
26 |       projectKey: 'TEST',
27 |     });
28 | 
29 |     expect(result).toHaveProperty('projectKey', 'TEST');
30 |     expect(result).toHaveProperty('name', 'Test Project');
31 |   });
32 | 
33 |   it('calls backlog.deleteProject with correct params when using project key', async () => {
34 |     await tool.handler({
35 |       projectKey: 'TEST',
36 |     });
37 | 
38 |     expect(mockBacklog.deleteProject).toHaveBeenCalledWith('TEST');
39 |   });
40 | 
41 |   it('calls backlog.deleteProject with correct params when using project ID', async () => {
42 |     await tool.handler({
43 |       projectId: 1,
44 |     });
45 | 
46 |     expect(mockBacklog.deleteProject).toHaveBeenCalledWith(1);
47 |   });
48 | 
49 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
50 |     const params = {}; // No identifier provided
51 | 
52 |     // Assuming resolveIdOrKey for "project" entity throws "Project ID or key is required"
53 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
54 |   });
55 | });
56 | 
```

--------------------------------------------------------------------------------
/src/createTranslationHelper.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { createTranslationHelper } from './createTranslationHelper';
 2 | import { writeFileSync, unlinkSync } from 'fs';
 3 | import { describe, it, expect, beforeEach } from '@jest/globals';
 4 | import path from 'path';
 5 | 
 6 | const TEMP_CONFIG_PATH = path.resolve(
 7 |   process.cwd(),
 8 |   '.backlog-mcp-serverrc.json'
 9 | );
10 | 
11 | describe('createTranslationHelper', () => {
12 |   beforeEach(() => {
13 |     delete process.env.BACKLOG_MCP_HELLO;
14 |     try {
15 |       unlinkSync(TEMP_CONFIG_PATH);
16 |     } catch {
17 |       // noop: cannot do anything
18 |     }
19 |   });
20 | 
21 |   it('returns fallback if no env or config is present', () => {
22 |     const { t } = createTranslationHelper({ searchDir: process.cwd() });
23 |     expect(t('HELLO', 'Fallback')).toBe('Fallback');
24 |   });
25 | 
26 |   it('returns value from config file if present', () => {
27 |     writeFileSync(
28 |       TEMP_CONFIG_PATH,
29 |       JSON.stringify({ HELLO: 'From config' }, null, 2),
30 |       'utf-8'
31 |     );
32 | 
33 |     const { t } = createTranslationHelper({ searchDir: process.cwd() });
34 |     expect(t('HELLO', 'Fallback')).toBe('From config');
35 |   });
36 | 
37 |   it('returns value from environment variable over config', () => {
38 |     writeFileSync(
39 |       TEMP_CONFIG_PATH,
40 |       JSON.stringify({ HELLO: 'From config' }, null, 2),
41 |       'utf-8'
42 |     );
43 | 
44 |     process.env.BACKLOG_MCP_HELLO = 'From env';
45 | 
46 |     const { t } = createTranslationHelper({ searchDir: process.cwd() });
47 |     expect(t('HELLO', 'Fallback')).toBe('From env');
48 |   });
49 | 
50 |   it('caches the first call to a key', () => {
51 |     process.env.BACKLOG_MCP_HELLO = 'Cached value';
52 |     const { t } = createTranslationHelper({ searchDir: process.cwd() });
53 | 
54 |     const first = t('HELLO', 'Fallback');
55 |     process.env.BACKLOG_MCP_HELLO = 'Modified value';
56 |     const second = t('HELLO', 'Fallback');
57 | 
58 |     expect(first).toBe('Cached value');
59 |     expect(second).toBe('Cached value');
60 |   });
61 | });
62 | 
```

--------------------------------------------------------------------------------
/src/backlog/customFields.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {
 2 |   customFieldsToPayload,
 3 |   type CustomFieldInput,
 4 | } from './customFields.js';
 5 | import { describe, it, expect } from '@jest/globals';
 6 | 
 7 | describe('customFieldsToPayload', () => {
 8 |   it('returns an empty object when input is undefined', () => {
 9 |     const result = customFieldsToPayload(undefined);
10 |     expect(result).toEqual({});
11 |   });
12 | 
13 |   it('returns an empty object when input is null', () => {
14 |     const result = customFieldsToPayload(null as any);
15 |     expect(result).toEqual({});
16 |   });
17 | 
18 |   it('converts single field with string value', () => {
19 |     const input: CustomFieldInput[] = [{ id: 100, value: 'test value' }];
20 |     const result = customFieldsToPayload(input);
21 |     expect(result).toEqual({
22 |       customField_100: 'test value',
23 |     });
24 |   });
25 | 
26 |   it('converts single field with number value', () => {
27 |     const input: CustomFieldInput[] = [{ id: 101, value: 42 }];
28 |     const result = customFieldsToPayload(input);
29 |     expect(result).toEqual({
30 |       customField_101: 42,
31 |     });
32 |   });
33 | 
34 |   it('converts single field with array value and otherValue', () => {
35 |     const input: CustomFieldInput[] = [
36 |       {
37 |         id: 102,
38 |         value: ['OptionA', 'OptionB'],
39 |         otherValue: 'custom input',
40 |       },
41 |     ];
42 |     const result = customFieldsToPayload(input);
43 |     expect(result).toEqual({
44 |       customField_102: ['OptionA', 'OptionB'],
45 |       customField_102_otherValue: 'custom input',
46 |     });
47 |   });
48 | 
49 |   it('converts multiple fields of mixed types', () => {
50 |     const input: CustomFieldInput[] = [
51 |       { id: 201, value: 'text' },
52 |       { id: 202, value: 123 },
53 |       { id: 203, value: '', otherValue: 'detail' },
54 |     ];
55 |     const result = customFieldsToPayload(input);
56 |     expect(result).toEqual({
57 |       customField_201: 'text',
58 |       customField_202: 123,
59 |       customField_203: '',
60 |       customField_203_otherValue: 'detail',
61 |     });
62 |   });
63 | });
64 | 
```

--------------------------------------------------------------------------------
/src/tools/getWatchingListItems.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getWatchingListItemsTool } from './getWatchingListItems.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getWatchingListItemsTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getWatchingListItems: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 1,
11 |         resourceAlreadyRead: false,
12 |         note: 'Important issue',
13 |         type: 'issue',
14 |         issue: {
15 |           id: 1000,
16 |           projectId: 100,
17 |           issueKey: 'TEST-1',
18 |           summary: 'Test issue',
19 |         },
20 |         created: '2023-01-01T00:00:00Z',
21 |         updated: '2023-01-01T00:00:00Z',
22 |       },
23 |       {
24 |         id: 2,
25 |         resourceAlreadyRead: true,
26 |         note: 'Important wiki',
27 |         type: 'wiki',
28 |         wiki: {
29 |           id: 2000,
30 |           projectId: 100,
31 |           name: 'Test wiki',
32 |           content: 'Wiki content',
33 |         },
34 |         created: '2023-01-02T00:00:00Z',
35 |         updated: '2023-01-02T00:00:00Z',
36 |       },
37 |     ]),
38 |   };
39 | 
40 |   const mockTranslationHelper = createTranslationHelper();
41 |   const tool = getWatchingListItemsTool(
42 |     mockBacklog as Backlog,
43 |     mockTranslationHelper
44 |   );
45 | 
46 |   it('returns watching list items as formatted JSON text', async () => {
47 |     const result = await tool.handler({
48 |       userId: 1,
49 |     });
50 | 
51 |     if (!Array.isArray(result)) {
52 |       throw new Error('Unexpected non array result');
53 |     }
54 |     expect(result).toHaveLength(2);
55 |     expect(result[0].note).toContain('Important issue');
56 |     expect(result[1].note).toContain('Important wiki');
57 |   });
58 | 
59 |   it('calls backlog.getWatchingListItems with correct params', async () => {
60 |     await tool.handler({
61 |       userId: 1,
62 |     });
63 | 
64 |     expect(mockBacklog.getWatchingListItems).toHaveBeenCalledWith(1);
65 |   });
66 | });
67 | 
```

--------------------------------------------------------------------------------
/src/utils/resolveIdOrKey.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { TranslationHelper } from '../createTranslationHelper.js';
 2 | 
 3 | export type EntityName = 'issue' | 'project' | 'repository';
 4 | 
 5 | type ResolveResult =
 6 |   | { ok: true; value: string | number }
 7 |   | { ok: false; error: Error };
 8 | 
 9 | type ResolveIdOrFieldInput<F extends string> = {
10 |   id?: number;
11 | } & {
12 |   [K in F]?: string;
13 | };
14 | 
15 | /**
16 |  * Generic resolver for entity identification by ID or named field (e.g., key, name, slug).
17 |  * @param entity - The entity name, e.g., "project"
18 |  * @param fieldName - The name of the alternative to `id`, e.g., "key", "name", "slug"
19 |  * @param values - An object with `id?: number` and `[fieldName]?: string`
20 |  * @param t - Translator
21 |  */
22 | function resolveIdOrField<E extends EntityName, F extends string>(
23 |   entity: E,
24 |   fieldName: F,
25 |   values: ResolveIdOrFieldInput<F>,
26 |   t: TranslationHelper['t']
27 | ): ResolveResult {
28 |   const value = tryResolveIdOrField(fieldName, values);
29 |   if (value === undefined) {
30 |     return {
31 |       ok: false,
32 |       error: new Error(
33 |         t(
34 |           `${entity.toUpperCase()}_ID_OR_${fieldName.toUpperCase()}_REQUIRED`,
35 |           `${capitalize(entity)} ID or ${fieldName} is required`
36 |         )
37 |       ),
38 |     };
39 |   }
40 | 
41 |   return { ok: true, value };
42 | }
43 | 
44 | function tryResolveIdOrField<F extends string>(
45 |   fieldName: F,
46 |   values: ResolveIdOrFieldInput<F>
47 | ): string | number | undefined {
48 |   return values.id !== undefined ? values.id : values[fieldName];
49 | }
50 | 
51 | export const resolveIdOrKey = <E extends EntityName>(
52 |   entity: E,
53 |   values: { id?: number; key?: string },
54 |   t: TranslationHelper['t']
55 | ): ResolveResult => resolveIdOrField(entity, 'key', values, t);
56 | 
57 | export const resolveIdOrName = <E extends EntityName>(
58 |   entity: E,
59 |   values: { id?: number; name?: string },
60 |   t: TranslationHelper['t']
61 | ): ResolveResult => resolveIdOrField(entity, 'name', values, t);
62 | 
63 | function capitalize(str: string): string {
64 |   return str.charAt(0).toUpperCase() + str.slice(1);
65 | }
66 | 
```

--------------------------------------------------------------------------------
/src/tools/addIssueComment.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { IssueCommentSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const addIssueCommentSchema = buildToolSchema((t) => ({
 9 |   issueId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_ADD_ISSUE_COMMENT_ID',
15 |         'The numeric ID of the issue (e.g., 12345)'
16 |       )
17 |     ),
18 |   issueKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t('TOOL_ADD_ISSUE_COMMENT_KEY', "The key of the issue (e.g., 'PROJ-123')")
23 |     ),
24 |   content: z
25 |     .string()
26 |     .describe(t('TOOL_ADD_ISSUE_COMMENT_CONTENT', 'Comment content')),
27 |   notifiedUserId: z
28 |     .array(z.number())
29 |     .optional()
30 |     .describe(
31 |       t('TOOL_ADD_ISSUE_COMMENT_NOTIFIED_USER_ID', 'User IDs to notify')
32 |     ),
33 |   attachmentId: z
34 |     .array(z.number())
35 |     .optional()
36 |     .describe(t('TOOL_ADD_ISSUE_COMMENT_ATTACHMENT_ID', 'Attachment IDs')),
37 | }));
38 | 
39 | export const addIssueCommentTool = (
40 |   backlog: Backlog,
41 |   { t }: TranslationHelper
42 | ): ToolDefinition<
43 |   ReturnType<typeof addIssueCommentSchema>,
44 |   (typeof IssueCommentSchema)['shape']
45 | > => {
46 |   return {
47 |     name: 'add_issue_comment',
48 |     description: t(
49 |       'TOOL_ADD_ISSUE_COMMENT_DESCRIPTION',
50 |       'Adds a comment to an issue'
51 |     ),
52 |     schema: z.object(addIssueCommentSchema(t)),
53 |     outputSchema: IssueCommentSchema,
54 |     handler: async ({
55 |       issueId,
56 |       issueKey,
57 |       content,
58 |       notifiedUserId,
59 |       attachmentId,
60 |     }) => {
61 |       const result = resolveIdOrKey('issue', { id: issueId, key: issueKey }, t);
62 |       if (!result.ok) {
63 |         throw result.error;
64 |       }
65 |       return backlog.postIssueComments(result.value, {
66 |         content,
67 |         notifiedUserId,
68 |         attachmentId,
69 |       });
70 |     },
71 |   };
72 | };
73 | 
```

--------------------------------------------------------------------------------
/src/tools/getIssueTypes.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getIssueTypesTool } from './getIssueTypes.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getIssueTypesTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getIssueTypes: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 1,
11 |         projectId: 100,
12 |         name: 'Bug',
13 |         color: '#990000',
14 |       },
15 |       {
16 |         id: 2,
17 |         projectId: 100,
18 |         name: 'Task',
19 |         color: '#7ea800',
20 |       },
21 |       {
22 |         id: 3,
23 |         projectId: 100,
24 |         name: 'Request',
25 |         color: '#ff9200',
26 |       },
27 |     ]),
28 |   };
29 | 
30 |   const mockTranslationHelper = createTranslationHelper();
31 |   const tool = getIssueTypesTool(mockBacklog as Backlog, mockTranslationHelper);
32 | 
33 |   it('returns issue types list as formatted JSON text', async () => {
34 |     const result = await tool.handler({
35 |       projectKey: 'TEST',
36 |     });
37 | 
38 |     if (!Array.isArray(result)) {
39 |       throw new Error('Unexpected non array result');
40 |     }
41 |     expect(result).toHaveLength(3);
42 |     expect(result[0].name).toContain('Bug');
43 |     expect(result[1].name).toContain('Task');
44 |     expect(result[2].name).toContain('Request');
45 |   });
46 | 
47 |   it('calls backlog.getIssueTypes with correct params when using project key', async () => {
48 |     await tool.handler({
49 |       projectKey: 'TEST',
50 |     });
51 | 
52 |     expect(mockBacklog.getIssueTypes).toHaveBeenCalledWith('TEST');
53 |   });
54 | 
55 |   it('calls backlog.getIssueTypes with correct params when using project ID', async () => {
56 |     await tool.handler({
57 |       projectId: 100,
58 |     });
59 | 
60 |     expect(mockBacklog.getIssueTypes).toHaveBeenCalledWith(100);
61 |   });
62 | 
63 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
64 |     const params = {}; // No identifier provided
65 | 
66 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
67 |   });
68 | });
69 | 
```

--------------------------------------------------------------------------------
/src/utils/toolsetUtils.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Backlog } from 'backlog-js';
 2 | import { ToolsetGroup, Toolset } from '../types/toolsets.js';
 3 | import { allTools } from '../tools/tools.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | export function getToolset(
 7 |   group: ToolsetGroup,
 8 |   name: string
 9 | ): Toolset | undefined {
10 |   return group.toolsets.find((t) => t.name === name);
11 | }
12 | 
13 | export function enableToolset(group: ToolsetGroup, name: string): string {
14 |   const ts = getToolset(group, name);
15 |   if (!ts) return `Toolset ${name} not found`;
16 |   if (ts.enabled) return `Toolset ${name} is already enabled`;
17 |   ts.enabled = true;
18 |   return `Toolset ${name} enabled`;
19 | }
20 | 
21 | export function getEnabledTools(group: ToolsetGroup) {
22 |   return group.toolsets.filter((ts) => ts.enabled).flatMap((ts) => ts.tools);
23 | }
24 | 
25 | export function listAvailableToolsets(group: ToolsetGroup) {
26 |   return group.toolsets.map((ts) => ({
27 |     name: ts.name,
28 |     description: ts.description,
29 |     currentlyEnabled: ts.enabled,
30 |     canEnable: true,
31 |   }));
32 | }
33 | 
34 | export function listToolsetTools(group: ToolsetGroup, name: string) {
35 |   const ts = getToolset(group, name);
36 |   return (
37 |     ts?.tools.map((tool) => ({
38 |       name: tool.name,
39 |       description: tool.description,
40 |       toolset: name,
41 |       canEnable: true,
42 |     })) ?? []
43 |   );
44 | }
45 | 
46 | export const buildToolsetGroup = (
47 |   backlog: Backlog,
48 |   helper: TranslationHelper,
49 |   enabledToolsets: string[]
50 | ): ToolsetGroup => {
51 |   const toolsetGroup = allTools(backlog, helper);
52 |   const knownNames = toolsetGroup.toolsets.map((ts) => ts.name);
53 |   const unknown = enabledToolsets.filter(
54 |     (name) => name !== 'all' && !knownNames.includes(name)
55 |   );
56 | 
57 |   if (unknown.length > 0) {
58 |     console.warn(`⚠️ Unknown toolsets: ${unknown.join(', ')}`);
59 |   }
60 | 
61 |   const allEnabled = enabledToolsets.includes('all');
62 | 
63 |   return {
64 |     toolsets: toolsetGroup.toolsets.map((ts) => ({
65 |       ...ts,
66 |       enabled: allEnabled || enabledToolsets.includes(ts.name),
67 |     })),
68 |   };
69 | };
70 | 
```

--------------------------------------------------------------------------------
/src/tools/deleteIssue.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { deleteIssueTool } from './deleteIssue.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('deleteIssueTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     deleteIssue: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: 1,
10 |       projectId: 100,
11 |       issueKey: 'TEST-1',
12 |       keyId: 1,
13 |       issueType: {
14 |         id: 2,
15 |         projectId: 100,
16 |         name: 'Bug',
17 |         color: '#990000',
18 |         displayOrder: 0,
19 |       },
20 |       summary: 'Test Issue',
21 |       description: 'This is a test issue',
22 |       status: {
23 |         id: 1,
24 |         name: 'Open',
25 |         projectId: 100,
26 |         color: '#ff0000',
27 |         displayOrder: 0,
28 |       },
29 |       priority: {
30 |         id: 3,
31 |         name: 'Normal',
32 |       },
33 |       created: '2023-01-01T00:00:00Z',
34 |       updated: '2023-01-01T00:00:00Z',
35 |     }),
36 |   };
37 | 
38 |   const mockTranslationHelper = createTranslationHelper();
39 |   const tool = deleteIssueTool(mockBacklog as Backlog, mockTranslationHelper);
40 | 
41 |   it('returns deleted issue information', async () => {
42 |     const result = await tool.handler({
43 |       issueKey: 'TEST-1',
44 |     });
45 | 
46 |     expect(result).toHaveProperty('issueKey', 'TEST-1');
47 |     expect(result).toHaveProperty('summary', 'Test Issue');
48 |   });
49 | 
50 |   it('calls backlog.deleteIssue with correct params when using issue key', async () => {
51 |     await tool.handler({
52 |       issueKey: 'TEST-1',
53 |     });
54 | 
55 |     expect(mockBacklog.deleteIssue).toHaveBeenCalledWith('TEST-1');
56 |   });
57 | 
58 |   it('calls backlog.deleteIssue with correct params when using issue ID', async () => {
59 |     await tool.handler({
60 |       issueId: 1,
61 |     });
62 | 
63 |     expect(mockBacklog.deleteIssue).toHaveBeenCalledWith(1); // Expect number
64 |   });
65 | 
66 |   it('throws an error if neither issueId nor issueKey is provided', async () => {
67 |     await expect(tool.handler({})).rejects.toThrow(Error);
68 |   });
69 | });
70 | 
```

--------------------------------------------------------------------------------
/src/tools/getIssueComments.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { IssueCommentSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getIssueCommentsSchema = buildToolSchema((t) => ({
 9 |   issueId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_ISSUE_COMMENTS_ISSUE_ID',
15 |         'The numeric ID of the issue (e.g., 12345)'
16 |       )
17 |     ),
18 |   issueKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_ISSUE_COMMENTS_ISSUE_KEY',
24 |         "The key of the issue (e.g., 'PROJ-123')"
25 |       )
26 |     ),
27 |   minId: z
28 |     .number()
29 |     .optional()
30 |     .describe(t('TOOL_GET_ISSUE_COMMENTS_MIN_ID', 'Minimum comment ID')),
31 |   maxId: z
32 |     .number()
33 |     .optional()
34 |     .describe(t('TOOL_GET_ISSUE_COMMENTS_MAX_ID', 'Maximum comment ID')),
35 |   count: z
36 |     .number()
37 |     .optional()
38 |     .describe(
39 |       t('TOOL_GET_ISSUE_COMMENTS_COUNT', 'Number of comments to retrieve')
40 |     ),
41 |   order: z
42 |     .enum(['asc', 'desc'])
43 |     .optional()
44 |     .describe(t('TOOL_GET_ISSUE_COMMENTS_ORDER', 'Sort order')),
45 | }));
46 | 
47 | export const getIssueCommentsTool = (
48 |   backlog: Backlog,
49 |   { t }: TranslationHelper
50 | ): ToolDefinition<
51 |   ReturnType<typeof getIssueCommentsSchema>,
52 |   (typeof IssueCommentSchema)['shape']
53 | > => {
54 |   return {
55 |     name: 'get_issue_comments',
56 |     description: t(
57 |       'TOOL_GET_ISSUE_COMMENTS_DESCRIPTION',
58 |       'Returns list of comments for an issue'
59 |     ),
60 |     schema: z.object(getIssueCommentsSchema(t)),
61 |     outputSchema: IssueCommentSchema,
62 |     handler: async ({ issueId, issueKey, ...params }) => {
63 |       const result = resolveIdOrKey('issue', { id: issueId, key: issueKey }, t);
64 |       if (!result.ok) {
65 |         throw result.error;
66 |       }
67 |       return backlog.getIssueComments(result.value, params);
68 |     },
69 |   };
70 | };
71 | 
```

--------------------------------------------------------------------------------
/.clinerules/commit-conventional-format.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Conventional Commit Format Guide
 2 | 
 3 | This document describes the conventional commit message format. Use this as a reference for generating or validating commit messages via an LLM (Large Language Model).
 4 | 
 5 | ## Format
 6 | 
 7 | Each commit message should follow the structure:
 8 | 
 9 | ```
10 | <type>[optional scope]: <description>
11 | 
12 | [optional body]
13 | 
14 | [optional footer(s)]
15 | ```
16 | 
17 | ### Examples
18 | 
19 | ```
20 | feat: add login button component
21 | fix(auth): handle token expiration error
22 | docs(readme): update setup instructions
23 | refactor(api): simplify request handler logic
24 | ```
25 | 
26 | ## Types
27 | 
28 | Use the following standard types:
29 | 
30 | - `feat`: A new feature  
31 | - `fix`: A bug fix  
32 | - `docs`: Documentation-only changes  
33 | - `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc)  
34 | - `refactor`: A code change that neither fixes a bug nor adds a feature  
35 | - `perf`: A code change that improves performance  
36 | - `test`: Adding missing tests or correcting existing tests  
37 | - `chore`: Changes to the build process or auxiliary tools and libraries  
38 | - `ci`: Changes to CI configuration files and scripts  
39 | - `build`: Changes that affect the build system or external dependencies  
40 | 
41 | ## Scope (Optional)
42 | 
43 | The scope specifies the module or area affected by the change, such as `auth`, `api`, `db`, etc.
44 | 
45 | Example:
46 | 
47 | ```
48 | fix(auth): re-validate session token after refresh
49 | ```
50 | 
51 | ## Description
52 | 
53 | Keep it short and imperative, like a commit title.  
54 | Do not capitalize the first letter unless it's a proper noun, and do not add a period at the end.
55 | 
56 | ## Body (Optional)
57 | 
58 | Explain what and why, not how.  
59 | Use bullet points if helpful.
60 | 
61 | ## Footer (Optional)
62 | 
63 | Used for breaking changes or issue references.
64 | 
65 | Examples:
66 | 
67 | ```
68 | BREAKING CHANGE: auth tokens are now rotated every hour
69 | ```
70 | 
71 | ```
72 | Closes #123
73 | ```
74 | 
75 | ## Summary
76 | 
77 | Follow this format strictly when generating commit messages programmatically or interacting with a Git workflow tool powered by LLMs. This helps ensure consistent, parsable, and meaningful commit history.
78 | 
```

--------------------------------------------------------------------------------
/src/tools/getCategories.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getCategoriesTool } from './getCategories.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getCategoriesTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getCategories: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 1,
11 |         name: 'Bug',
12 |         displayOrder: 0,
13 |       },
14 |       {
15 |         id: 2,
16 |         name: 'Feature',
17 |         displayOrder: 1,
18 |       },
19 |       {
20 |         id: 3,
21 |         name: 'Support',
22 |         displayOrder: 2,
23 |       },
24 |     ]),
25 |   };
26 | 
27 |   const mockTranslationHelper = createTranslationHelper();
28 |   const tool = getCategoriesTool(mockBacklog as Backlog, mockTranslationHelper);
29 | 
30 |   it('returns categories list as formatted JSON text', async () => {
31 |     const result = await tool.handler({
32 |       projectKey: 'TEST',
33 |     });
34 | 
35 |     if (!Array.isArray(result)) {
36 |       throw new Error('Unexpected non array result');
37 |     }
38 | 
39 |     expect(result).toHaveLength(3);
40 |     expect(result[0].name).toContain('Bug');
41 |     expect(result[1].name).toContain('Feature');
42 |     expect(result[2].name).toContain('Support');
43 |   });
44 | 
45 |   it('calls backlog.getCategories with correct params when using project key', async () => {
46 |     await tool.handler({
47 |       projectKey: 'TEST',
48 |     });
49 | 
50 |     expect(mockBacklog.getCategories).toHaveBeenCalledWith('TEST');
51 |   });
52 | 
53 |   it('calls backlog.getCategories with correct params when using project ID', async () => {
54 |     await tool.handler({
55 |       projectId: 100,
56 |     });
57 | 
58 |     expect(mockBacklog.getCategories).toHaveBeenCalledWith(100);
59 |   });
60 | 
61 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
62 |     const params = {}; // No identifier provided
63 | 
64 |     // Assuming resolveIdOrKey for "project" entity (as categories are project-specific)
65 |     // throws "Project ID or key is required"
66 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
67 |   });
68 | });
69 | 
```

--------------------------------------------------------------------------------
/src/tools/getGitRepository.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Backlog } from 'backlog-js';
 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
 4 | import { TranslationHelper } from '../createTranslationHelper.js';
 5 | import { GitRepositorySchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getGitRepositorySchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_GIT_REPOSITORY_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_GIT_REPOSITORY_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 |   repoId: z
28 |     .number()
29 |     .optional()
30 |     .describe(t('TOOL_GET_GIT_REPOSITORY_REPO_ID', 'Repository ID')),
31 |   repoName: z
32 |     .string()
33 |     .optional()
34 |     .describe(t('TOOL_GET_GIT_REPOSITORY_REPO_NAME', 'Repository name')),
35 | }));
36 | 
37 | export const getGitRepositoryTool = (
38 |   backlog: Backlog,
39 |   { t }: TranslationHelper
40 | ): ToolDefinition<
41 |   ReturnType<typeof getGitRepositorySchema>,
42 |   (typeof GitRepositorySchema)['shape']
43 | > => {
44 |   return {
45 |     name: 'get_git_repository',
46 |     description: t(
47 |       'TOOL_GET_GIT_REPOSITORY_DESCRIPTION',
48 |       'Returns information about a specific Git repository'
49 |     ),
50 |     schema: z.object(getGitRepositorySchema(t)),
51 |     outputSchema: GitRepositorySchema,
52 |     handler: async ({ projectId, projectKey, repoId, repoName }) => {
53 |       const result = resolveIdOrKey(
54 |         'project',
55 |         { id: projectId, key: projectKey },
56 |         t
57 |       );
58 |       if (!result.ok) {
59 |         throw result.error;
60 |       }
61 |       const repoResult = resolveIdOrName(
62 |         'repository',
63 |         { id: repoId, name: repoName },
64 |         t
65 |       );
66 |       if (!repoResult.ok) {
67 |         throw repoResult.error;
68 |       }
69 |       return backlog.getGitRepository(result.value, String(repoResult.value));
70 |     },
71 |   };
72 | };
73 | 
```

--------------------------------------------------------------------------------
/src/utils/toolsetUtils.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, expect, it } from '@jest/globals';
 2 | 
 3 | import { ToolsetGroup } from '../types/toolsets.js';
 4 | import {
 5 |   enableToolset,
 6 |   getEnabledTools,
 7 |   getToolset,
 8 |   listAvailableToolsets,
 9 |   listToolsetTools,
10 | } from '../utils/toolsetUtils.js';
11 | 
12 | const mockTool = {
13 |   name: 'mock_tool',
14 |   description: 'A mock tool',
15 |   schema: { shape: {} },
16 |   handler: async () => ({ content: [] }),
17 |   outputSchema: {},
18 | };
19 | 
20 | const toolsetGroup: ToolsetGroup = {
21 |   toolsets: [
22 |     {
23 |       name: 'test_set',
24 |       description: 'Test set',
25 |       enabled: false,
26 |       tools: [mockTool as unknown as any],
27 |     },
28 |   ],
29 | };
30 | 
31 | describe('Toolset Utils', () => {
32 |   it('getToolset returns correct toolset', () => {
33 |     const ts = getToolset(toolsetGroup, 'test_set');
34 |     expect(ts).toBeDefined();
35 |     expect(ts?.name).toBe('test_set');
36 |   });
37 | 
38 |   it('enableToolset enables a toolset', () => {
39 |     const msg = enableToolset(toolsetGroup, 'test_set');
40 |     expect(msg).toContain('enabled');
41 |     expect(getToolset(toolsetGroup, 'test_set')?.enabled).toBe(true);
42 |   });
43 | 
44 |   it('enableToolset returns already enabled message', () => {
45 |     const msg = enableToolset(toolsetGroup, 'test_set');
46 |     expect(msg).toContain('already enabled');
47 |   });
48 | 
49 |   it('getEnabledTools returns enabled tools', () => {
50 |     const tools = getEnabledTools(toolsetGroup);
51 |     expect(tools.length).toBe(1);
52 |     expect(tools[0].name).toBe('mock_tool');
53 |   });
54 | 
55 |   it('listAvailableToolsets returns all toolsets', () => {
56 |     const list = listAvailableToolsets(toolsetGroup);
57 |     expect(list.length).toBe(1);
58 |     expect(list[0].name).toBe('test_set');
59 |     expect(list[0].currentlyEnabled).toBe(true);
60 |   });
61 | 
62 |   it('listToolsetTools returns tools of a toolset', () => {
63 |     const tools = listToolsetTools(toolsetGroup, 'test_set');
64 |     expect(tools.length).toBe(1);
65 |     expect(tools[0].name).toBe('mock_tool');
66 |   });
67 | 
68 |   it('listToolsetTools returns empty for unknown toolset', () => {
69 |     const tools = listToolsetTools(toolsetGroup, 'unknown');
70 |     expect(tools.length).toBe(0);
71 |   });
72 | });
73 | 
```

--------------------------------------------------------------------------------
/src/tools/addWiki.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { addWikiTool } from './addWiki.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('addWikiTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     postWiki: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: 1,
10 |       projectId: 100,
11 |       name: 'Getting Started',
12 |       content: '# Welcome to the project\n\nThis is a wiki page.',
13 |       createdUser: {
14 |         id: 1,
15 |         userId: 'admin',
16 |         name: 'Admin User',
17 |         roleType: 1,
18 |         lang: 'en',
19 |         mailAddress: '[email protected]',
20 |       },
21 |       created: '2023-01-01T00:00:00Z',
22 |       updatedUser: {
23 |         id: 1,
24 |         userId: 'admin',
25 |         name: 'Admin User',
26 |         roleType: 1,
27 |         lang: 'en',
28 |         mailAddress: '[email protected]',
29 |       },
30 |       updated: '2023-01-01T00:00:00Z',
31 |     }),
32 |   };
33 | 
34 |   const mockTranslationHelper = createTranslationHelper();
35 |   const tool = addWikiTool(mockBacklog as Backlog, mockTranslationHelper);
36 | 
37 |   it('returns created wiki as formatted JSON text', async () => {
38 |     const result = await tool.handler({
39 |       projectId: 100,
40 |       name: 'Getting Started',
41 |       content: '# Welcome to the project\n\nThis is a wiki page.',
42 |       mailNotify: false,
43 |     });
44 | 
45 |     if (Array.isArray(result)) {
46 |       throw new Error('Unexpected array result');
47 |     }
48 |     expect(result.name).toEqual('Getting Started');
49 |     expect(result.content).toContain('Welcome to the project');
50 |   });
51 | 
52 |   it('calls backlog.postWiki with correct params', async () => {
53 |     const params = {
54 |       projectId: 100,
55 |       name: 'Getting Started',
56 |       content: '# Welcome to the project\n\nThis is a wiki page.',
57 |       mailNotify: false,
58 |     };
59 | 
60 |     await tool.handler(params);
61 | 
62 |     expect(mockBacklog.postWiki).toHaveBeenCalledWith({
63 |       projectId: 100,
64 |       name: 'Getting Started',
65 |       content: '# Welcome to the project\n\nThis is a wiki page.',
66 |       mailNotify: false,
67 |     });
68 |   });
69 | });
70 | 
```

--------------------------------------------------------------------------------
/src/handlers/transformers/wrapWithFieldPicking.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { parse, SelectionSetNode } from 'graphql';
 2 | import { isErrorLike, SafeResult } from '../../types/result.js';
 3 | 
 4 | export function wrapWithFieldPicking<I extends { fields?: string }, O>(
 5 |   fn: (input: I) => Promise<SafeResult<O>>
 6 | ): (input: I) => Promise<SafeResult<O>> {
 7 |   return async (input: I) => {
 8 |     const { fields, ...rest } = input;
 9 |     const result = await fn(rest as I);
10 | 
11 |     if (!fields || isErrorLike(result)) {
12 |       return result;
13 |     }
14 | 
15 |     const selectionSet = parseFieldsSelection(fields);
16 |     const resultData = result.data;
17 | 
18 |     if (Array.isArray(resultData)) {
19 |       return {
20 |         kind: 'ok',
21 |         data: resultData.map((item) =>
22 |           pickFieldsFromData(item, selectionSet)
23 |         ) as unknown as O,
24 |       };
25 |     } else if (typeof result === 'object' && result !== null) {
26 |       return {
27 |         kind: 'ok',
28 |         data: pickFieldsFromData(
29 |           resultData as Record<string, unknown>,
30 |           selectionSet
31 |         ) as O,
32 |       };
33 |     } else {
34 |       return result;
35 |     }
36 |   };
37 | }
38 | 
39 | function parseFieldsSelection(fieldsString: string): SelectionSetNode {
40 |   const query = `query Dummy ${fieldsString}`;
41 |   const ast = parse(query);
42 |   const opDef = ast.definitions[0];
43 |   if (opDef.kind !== 'OperationDefinition' || !opDef.selectionSet) {
44 |     throw new Error('Invalid GraphQL fields');
45 |   }
46 |   return opDef.selectionSet;
47 | }
48 | 
49 | function pickFieldsFromData(
50 |   data: Record<string, unknown> | null | undefined,
51 |   selectionSet: SelectionSetNode
52 | ): Record<string, unknown> {
53 |   const result: Record<string, unknown> = {};
54 | 
55 |   for (const selection of selectionSet.selections) {
56 |     if (selection.kind === 'Field') {
57 |       const key = selection.name.value;
58 |       if (data != null && key in data) {
59 |         const value = data[key];
60 |         if (selection.selectionSet && value != null) {
61 |           result[key] = pickFieldsFromData(
62 |             data[key] as Record<string, unknown>,
63 |             selection.selectionSet
64 |           );
65 |         } else {
66 |           result[key] = data[key];
67 |         }
68 |       }
69 |     }
70 |   }
71 | 
72 |   return result;
73 | }
74 | 
```

--------------------------------------------------------------------------------
/src/tools/getDocumentTree.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getDocumentTreeTool } from './getDocumentTree.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | // export const DocumentTreeFullSchema = z.object({
 6 | //   projectId: z.string(),
 7 | //   activeTree: ActiveTrashTreeSchema.optional(),
 8 | //   trashTree: ActiveTrashTreeSchema.optional(),
 9 | // });
10 | describe('getDocumentTreeTool', () => {
11 |   const mockBacklog: Partial<Backlog> = {
12 |     getDocumentTree: jest.fn<() => Promise<any>>().mockResolvedValue({
13 |       projectId: 1,
14 |       activeTree: {
15 |         id: 'Active',
16 |         children: [
17 |           {
18 |             id: '01934345404771adb2113d7792bb4351',
19 |             name: 'local test',
20 |             children: [
21 |               {
22 |                 id: '019347fc760c7b0abff04b44628c94d7',
23 |                 name: 'test2',
24 |                 children: [
25 |                   {
26 |                     id: '0192ff5990da76c289dee06b1f11fa01',
27 |                     name: 'aaatest234',
28 |                     children: [],
29 |                     emoji: '',
30 |                   },
31 |                 ],
32 |                 emoji: '',
33 |               },
34 |             ],
35 |             emoji: '',
36 |           },
37 |         ],
38 |       },
39 |       trashTree: {},
40 |     }),
41 |   };
42 | 
43 |   const mockTranslationHelper = createTranslationHelper();
44 |   const tool = getDocumentTreeTool(
45 |     mockBacklog as Backlog,
46 |     mockTranslationHelper
47 |   );
48 | 
49 |   it('returns document tree as formatted JSON text', async () => {
50 |     const result = await tool.handler({ projectIdOrKey: 'TEST_PROJECT' });
51 |     if (Array.isArray(result)) {
52 |       throw new Error('Unexpected array result');
53 |     }
54 | 
55 |     expect(result.projectId).toEqual(1);
56 |     expect(result.activeTree?.children).toHaveLength(1);
57 |     expect(result.activeTree?.children[0].children).toHaveLength(1);
58 |   });
59 | 
60 |   it('calls backlog.getDocumentTree with correct params', async () => {
61 |     await tool.handler({ projectIdOrKey: 'TEST_PROJECT' });
62 | 
63 |     expect(mockBacklog.getDocumentTree).toHaveBeenCalledWith('TEST_PROJECT');
64 |   });
65 | });
66 | 
```

--------------------------------------------------------------------------------
/src/tools/addProject.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { addProjectTool } from './addProject.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('addProjectTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     postProject: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: 1,
10 |       projectKey: 'TEST',
11 |       name: 'Test Project',
12 |       chartEnabled: true,
13 |       subtaskingEnabled: true,
14 |       projectLeaderCanEditProjectLeader: false,
15 |       textFormattingRule: 'backlog',
16 |       archived: false,
17 |       displayOrder: 0,
18 |     }),
19 |   };
20 | 
21 |   const mockTranslationHelper = createTranslationHelper();
22 |   const tool = addProjectTool(mockBacklog as Backlog, mockTranslationHelper);
23 | 
24 |   it('returns created project as formatted JSON text', async () => {
25 |     const result = await tool.handler({
26 |       name: 'Test Project',
27 |       key: 'TEST',
28 |       chartEnabled: true,
29 |       subtaskingEnabled: true,
30 |     });
31 |     if (Array.isArray(result)) {
32 |       throw new Error('Unexpected array result');
33 |     }
34 |     expect(result.name).toEqual('Test Project');
35 |     expect(result.projectKey).toEqual('TEST');
36 |   });
37 | 
38 |   it('calls backlog.postProject with correct params', async () => {
39 |     await tool.handler({
40 |       name: 'Test Project',
41 |       key: 'TEST',
42 |       chartEnabled: true,
43 |       subtaskingEnabled: true,
44 |     });
45 | 
46 |     expect(mockBacklog.postProject).toHaveBeenCalledWith({
47 |       name: 'Test Project',
48 |       key: 'TEST',
49 |       chartEnabled: true,
50 |       subtaskingEnabled: true,
51 |       projectLeaderCanEditProjectLeader: false,
52 |       textFormattingRule: 'backlog',
53 |     });
54 |   });
55 | 
56 |   it('uses default values for optional parameters', async () => {
57 |     await tool.handler({
58 |       name: 'Test Project',
59 |       key: 'TEST',
60 |     });
61 | 
62 |     expect(mockBacklog.postProject).toHaveBeenCalledWith({
63 |       name: 'Test Project',
64 |       key: 'TEST',
65 |       chartEnabled: false,
66 |       subtaskingEnabled: false,
67 |       projectLeaderCanEditProjectLeader: false,
68 |       textFormattingRule: 'backlog',
69 |     });
70 |   });
71 | });
72 | 
```
Page 1/3FirstPrevNextLast