# Directory Structure
```
├── .gitignore
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── api
│ │ └── backlogApi.ts
│ ├── config
│ │ └── config.ts
│ ├── core
│ │ ├── schema.ts
│ │ └── types.ts
│ ├── error
│ │ └── errors.ts
│ ├── index.ts
│ ├── services
│ │ ├── index.ts
│ │ ├── issueService.ts
│ │ ├── projectService.ts
│ │ └── wikiService.ts
│ └── tools
│ ├── handlers.ts
│ ├── index.ts
│ ├── registry.ts
│ └── toolDefinitions.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules/
build/
*.log
.env*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
> [!CAUTION]
> Official Backlog MCP server launched!
>
> This repository will be changed visible.
>
> https://nulab.com/ja/blog/backlog/released-backlog-mcp-server/
# Backlog MCP Server
An MCP server implementation that integrates the Backlog API.
## Tools
### Project API
- **backlog_get_projects**
- Execute projects get with pagination and filtering
- **backlog_get_project**
- Execute project gets with project id or key
### Issue API
- **backlog_get_issues**
- Execute issues get with pagination and filtering
- **backlog_get_issue**
- Execute issue gets with issue id or key
- **backlog_add_issue**
- Execute issue add with issue data
- **backlog_update_issue**
- Execute issue update with issue data
- **backlog_delete_issue**
- Execute issue delete with issue id or key
### Wiki API
- **backlog_get_wikis**
- Execute wikis get with keyword
- **backlog_get_wiki**
- Execute wiki gets with wiki id or key
- **backlog_add_wiki**
- Execute wiki add with wiki data
- **backlog_update_wiki**
- Execute wiki update with wiki data
- **backlog_delete_wiki**
- Execute wiki delete with wiki id or key
## Configuration
### Getting an API Key
1. Sign up for a [Backlog](https://backlog.com)
2. Choose a plan (Free plan available [here](https://registerjp.backlog.com/trial/with-new-account/plan/11))
3. Generate your API key from the individual settings [help](https://support-ja.backlog.com/hc/ja/articles/360035641754-API%E3%81%AE%E8%A8%AD%E5%AE%9A)
### Environment Variables
This server requires the following environment variables:
- Required:
- `BACKLOG_API_KEY`: Your Backlog API key
- `BACKLOG_SPACE_ID`: Your Backlog space ID
- Optional:
- `BACKLOG_BASE_URL`: Your Backlog base URL (default: `https://{your-space-id}.backlog.com/api/v2`)
### Usage with Claude Desktop
Add this to your `claude_desktop_config.json`:
#### NPX
```json
{
"mcpServers": {
"backlog": {
"command": "npx",
"args": [
"-y",
"backlog-mcp-server"
],
"env": {
"BACKLOG_API_KEY": "YOUR_API_KEY_HERE",
"BACKLOG_SPACE_ID": "YOUR_SPACE_ID_HERE"
}
}
}
}
```
#### Docker
```json
{
"mcpServers": {
"backlog": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"BACKLOG_API_KEY=YOUR_API_KEY_HERE",
"-e",
"BACKLOG_SPACE_ID=YOUR_SPACE_ID_HERE",
"mcp/backlog"
],
"env": {
"BACKLOG_API_KEY": "YOUR_API_KEY_HERE",
"BACKLOG_SPACE_ID": "YOUR_SPACE_ID_HERE"
}
}
}
}
```
## Development
### Installation
```bash
npm install
```
### Build
```bash
npm run build
```
### Debug
```bash
npm run debug
```
### Running Tests
T.B.D
### Docker Build
```bash
docker build -t mcp/backlog -f Dockerfile .
```
## Extending the Server
To add new tools:
1. Define a new Zod schema in `src/core/schema.ts`
2. Add a new tool definition in `src/tools/toolDefinitions.ts` and include it in `ALL_TOOLS`
3. Create a new handler in `src/tools/handlers.ts` and register it in `toolHandlers`
4. Implement business logic in a service in the `src/services/` directory
## License
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from "./toolDefinitions.js";
export * from "./handlers.js";
export * from "./registry.js";
```
--------------------------------------------------------------------------------
/src/services/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from "./issueService.js";
export * from "./projectService.js";
export * from "./wikiService.js";
```
--------------------------------------------------------------------------------
/src/config/config.ts:
--------------------------------------------------------------------------------
```typescript
export const BACKLOG_API_KEY = process.env.BACKLOG_API_KEY || "";
export const BACKLOG_SPACE_ID = process.env.BACKLOG_SPACE_ID || "";
export const BACKLOG_BASE_URL =
process.env.BACKLOG_BASE_URL ||
`https://${BACKLOG_SPACE_ID}.backlog.com/api/v2`;
export const SERVER_VERSION = "0.3.1";
export const SERVER_NAME = "backlog-mcp-server";
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM node:22.12-alpine AS builder
# Must be entire project because `prepare` script is run during `npm install` and requires all files.
COPY . /app
COPY tsconfig.json /tsconfig.json
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm npm install
RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
FROM node:22-alpine AS release
COPY --from=builder /app/build /app/build
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json
ENV NODE_ENV=production
WORKDIR /app
RUN npm ci --ignore-scripts --omit-dev
ENTRYPOINT ["node", "build/index.js"]
```
--------------------------------------------------------------------------------
/src/services/projectService.ts:
--------------------------------------------------------------------------------
```typescript
import { backlogAPI } from "../api/backlogApi.js";
import type { ProjectParams, ProjectsParams } from "../core/schema.js";
class ProjectService {
async getProjects(params: ProjectsParams): Promise<string> {
try {
return await backlogAPI.getProjects(params);
} catch (error) {
throw new Error(
`Failed to get projects: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async getProject(params: ProjectParams): Promise<string> {
try {
return await backlogAPI.getProject(params);
} catch (error) {
throw new Error(
`Failed to get project: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const projectService = new ProjectService();
```
--------------------------------------------------------------------------------
/src/tools/registry.ts:
--------------------------------------------------------------------------------
```typescript
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { ALL_TOOLS } from "./toolDefinitions.js";
class ToolRegistry {
private tools: Map<string, Tool>;
constructor(initialTools: Tool[] = []) {
this.tools = new Map();
for (const tool of initialTools) {
this.registerTool(tool);
}
}
registerTool(tool: Tool): void {
if (this.tools.has(tool.name)) {
console.warn(
`Tool with name "${tool.name}" is already registered. Overwriting...`,
);
}
this.tools.set(tool.name, tool);
}
getAllTools(): Tool[] {
return Array.from(this.tools.values());
}
getTool(name: string): Tool | undefined {
return this.tools.get(name);
}
hasTool(name: string): boolean {
return this.tools.has(name);
}
}
export const toolRegistry = new ToolRegistry(ALL_TOOLS);
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "backlog-mcp-server",
"version": "0.3.1",
"description": "MCP Server for Backlog API integration",
"license": "MIT",
"author": "Toshinori Suzuki <[email protected]>",
"repository": {
"type": "git",
"url": "git+https://github.com/fleagne/backlog-mcp-server.git"
},
"bugs": "https://github.com/fleagne/backlog-mcp-server/issues",
"keywords": [
"backlog",
"mcp"
],
"type": "module",
"bin": {
"mcp-server-backlog": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"prepare": "npm run build",
"watch": "tsc --watch",
"debug": "npm run build && npx @modelcontextprotocol/inspector build/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.6.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^22.13.9",
"typescript": "^5.8.2"
}
}
```
--------------------------------------------------------------------------------
/src/error/errors.ts:
--------------------------------------------------------------------------------
```typescript
class AppError extends Error {
public readonly statusCode: number;
public readonly isOperational: boolean;
constructor(message: string, statusCode = 500, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
export class APIError extends AppError {
constructor(message: string, statusCode = 500) {
super(`API Error: ${message}`, statusCode);
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(`Validation Error: ${message}`, 400);
}
}
export class ConfigurationError extends AppError {
constructor(message: string) {
super(`Configuration Error: ${message}`, 500, false);
}
}
export function formatError(error: unknown): string {
if (error instanceof AppError) {
return `${error.message} (Code: ${error.statusCode})`;
}
if (error instanceof Error) {
return error.message;
}
return String(error);
}
```
--------------------------------------------------------------------------------
/src/services/wikiService.ts:
--------------------------------------------------------------------------------
```typescript
import { backlogAPI } from "../api/backlogApi.js";
import type {
AddWikiParams,
DeleteWikiParams,
UpdateWikiParams,
WikiParams,
WikisParams,
} from "../core/schema.js";
class WikiService {
async getWikis(params: WikisParams): Promise<string> {
try {
return await backlogAPI.getWikis(params);
} catch (error) {
throw new Error(
`Failed to get wikis: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async getWiki(params: WikiParams): Promise<string> {
try {
return await backlogAPI.getWiki(params);
} catch (error) {
throw new Error(
`Failed to get wiki: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async addWiki(params: AddWikiParams): Promise<string> {
try {
return await backlogAPI.addWiki(params);
} catch (error) {
throw new Error(
`Failed to add wiki: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async updateWiki(params: UpdateWikiParams): Promise<string> {
try {
return await backlogAPI.updateWiki(params);
} catch (error) {
throw new Error(
`Failed to update wiki: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async deleteWiki(params: DeleteWikiParams): Promise<string> {
try {
return await backlogAPI.deleteWiki(params);
} catch (error) {
throw new Error(
`Failed to delete wiki: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const wikiService = new WikiService();
```
--------------------------------------------------------------------------------
/src/services/issueService.ts:
--------------------------------------------------------------------------------
```typescript
import { backlogAPI } from "../api/backlogApi.js";
import type {
AddIssueParams,
DeleteIssueParams,
IssueParams,
IssuesParams,
UpdateIssueParams,
} from "../core/schema.js";
class IssueService {
async getIssues(params: IssuesParams): Promise<string> {
try {
return await backlogAPI.getIssues(params);
} catch (error) {
throw new Error(
`Failed to get issues: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async getIssue(params: IssueParams): Promise<string> {
try {
return await backlogAPI.getIssue(params);
} catch (error) {
throw new Error(
`Failed to get issue: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async addIssue(params: AddIssueParams): Promise<string> {
try {
return await backlogAPI.addIssue(params);
} catch (error) {
throw new Error(
`Failed to add issue: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async updateIssue(params: UpdateIssueParams): Promise<string> {
try {
return await backlogAPI.updateIssue(params);
} catch (error) {
throw new Error(
`Failed to update issue: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async deleteIssue(params: DeleteIssueParams): Promise<string> {
try {
return await backlogAPI.deleteIssue(params);
} catch (error) {
throw new Error(
`Failed to delete issue: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const issueService = new IssueService();
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import {
BACKLOG_API_KEY,
BACKLOG_SPACE_ID,
SERVER_NAME,
SERVER_VERSION,
} from "./config/config.js";
import type { ToolName } from "./core/types.js";
import { ConfigurationError, formatError } from "./error/errors.js";
import { toolHandlers, toolRegistry } from "./tools/index.js";
if (BACKLOG_API_KEY === "" || BACKLOG_SPACE_ID === "") {
throw new ConfigurationError(
"BACKLOG_API_KEY or BACKLOG_SPACE_ID environment variable is required",
);
}
const server = new Server(
{
name: SERVER_NAME,
version: SERVER_VERSION,
},
{
capabilities: {
tools: {},
},
},
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: toolRegistry.getAllTools(),
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
if (!args) {
throw new Error("No arguments provided");
}
if (!toolRegistry.hasTool(name)) {
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true,
};
}
const handler = toolHandlers[name as ToolName];
if (handler) {
const toolResponse = await handler(args);
return { content: toolResponse.content, isError: toolResponse.isError };
}
return {
content: [{ type: "text", text: `No handler defined for tool: ${name}` }],
isError: true,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Server error: ${formatError(error)}`,
},
],
isError: true,
};
}
});
async function runServer() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Backlog MCP Server running on stdio");
} catch (error) {
console.error("Error starting server:", formatError(error));
process.exit(1);
}
}
runServer().catch((error) => {
console.error("Fatal error running server:", formatError(error));
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/core/types.ts:
--------------------------------------------------------------------------------
```typescript
interface BacklogUser {
id: number;
userId: string;
name: string;
roleType: number;
lang: string;
nulabAccount: {
nulabId: string;
name: string;
uniqueId: string;
};
mailAddress: string;
lastLoginTime: string;
}
interface BacklogIssueType {
id: number;
projectId: number;
name: string;
color: string;
displayOrder: number;
}
interface BacklogPriority {
id: number;
name: string;
}
interface BacklogMilestone {
id: number;
projectId: number;
name: string;
description: string;
startDate: string;
releaseDueDate: string;
archived: boolean;
displayOrder: number;
}
interface BacklogAttachment {
id: number;
name: string;
size: number;
}
interface BacklogSharedFile {
id: number;
projectId: number;
type: string;
dir: string;
name: string;
size: number;
createdUser: BacklogUser;
created: string;
updatedUser: BacklogUser;
updated: string;
}
interface BacklogStar {
id: number;
comment: string;
url: string;
title: string;
presenter: BacklogUser;
created: string;
}
interface BacklogCustomField {
id: number;
name: string;
value: string | number | null;
type: string;
}
export interface BacklogIssue {
id: number;
projectId: number;
issueKey: string;
keyId: number;
issueType: BacklogIssueType;
summary: string;
description: string;
resolution: string;
priority: BacklogPriority;
status: {
id: number;
name: string;
};
assignee: BacklogUser;
category: string[];
versions: string[];
milestone: BacklogMilestone[];
startDate: string;
dueDate: string;
estimatedHours: number;
actualHours: number;
parentIssueId: number;
createdUser: BacklogUser;
created: string;
updatedUser: BacklogUser;
updated: string;
customFields: BacklogCustomField[];
attachments: BacklogAttachment[];
sharedFiles: BacklogSharedFile[];
stars: BacklogStar[];
}
export interface BacklogProject {
id: number;
projectKey: string;
name: string;
chartEnabled: boolean;
useResolvedForChart: boolean;
subtaskingEnabled: boolean;
projectLeaderCanEditProjectLeader: boolean;
useWiki: boolean;
useFileSharing: boolean;
useWikiTreeView: boolean;
useOriginalImageSizeAtWiki: boolean;
useSubversion: boolean;
useGit: boolean;
textFormattingRule: string;
archived: boolean;
displayOrder: number;
useDevAttributes: boolean;
}
interface Tag {
id: number;
name: string;
}
export interface BacklogWiki {
id: number;
projectId: number;
name: string;
tags: Tag[];
createdUser: BacklogUser;
created: string;
updatedUser: BacklogUser;
updated: string;
}
export type ToolName =
| "backlog_get_projects"
| "backlog_get_project"
| "backlog_get_issues"
| "backlog_get_issue"
| "backlog_add_issue"
| "backlog_update_issue"
| "backlog_delete_issue"
| "backlog_get_wikis"
| "backlog_get_wiki"
| "backlog_add_wiki"
| "backlog_update_wiki"
| "backlog_delete_wiki";
```
--------------------------------------------------------------------------------
/src/api/backlogApi.ts:
--------------------------------------------------------------------------------
```typescript
import { BACKLOG_API_KEY, BACKLOG_BASE_URL } from "../config/config.js";
import type {
AddIssueParams,
AddWikiParams,
DeleteIssueParams,
DeleteWikiParams,
IssueParams,
IssuesParams,
ProjectParams,
ProjectsParams,
UpdateIssueParams,
UpdateWikiParams,
WikiParams,
WikisParams,
} from "../core/schema.js";
import type {
BacklogIssue,
BacklogProject,
BacklogWiki,
} from "../core/types.js";
import { APIError } from "../error/errors.js";
class BacklogAPI {
private baseUrl: string;
private apiKey: string;
constructor(baseUrl: string, apiKey: string) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
}
private async request<T>(
path: string,
// biome-ignore lint/suspicious/noExplicitAny: Because the interface changes according to the purpose
params: Record<string, any> = {},
method: "GET" | "POST" | "PATCH" | "DELETE" = "GET",
headers: Record<string, string> = {},
): Promise<T> {
const url = new URL(`${this.baseUrl}${path}`);
url.searchParams.set("apiKey", this.apiKey);
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null) continue;
if (Array.isArray(value)) {
for (const item of value) {
url.searchParams.append(`${key}[]`, String(item));
}
} else {
url.searchParams.set(key, String(value));
}
}
let bodyData: URLSearchParams | string | undefined;
if (method === "POST" || method === "PATCH") {
if (headers["Content-Type"] === "application/x-www-form-urlencoded") {
const formData = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null) continue;
if (Array.isArray(value)) {
for (const item of value) {
formData.append(`${key}[]`, String(item));
}
} else {
formData.append(key, String(value));
}
}
bodyData = formData;
} else {
bodyData = JSON.stringify(params);
}
}
try {
const response = await fetch(url, {
method: method,
headers: headers,
body: bodyData,
});
if (!response.ok) {
const errorText = await response.text();
throw new APIError(
`Backlog API responded with status: ${response.status} ${response.statusText}\n${errorText}`,
response.status,
);
}
return response.json() as Promise<T>;
} catch (error) {
if (error instanceof APIError) {
throw error;
}
throw new APIError(
`Failed to communicate with Backlog API: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async getProjects(params: ProjectsParams): Promise<string> {
const data = await this.request<BacklogProject[]>(
"/projects",
params,
"GET",
{
Accept: "application/json",
},
);
return JSON.stringify(data, null, 2);
}
async getProject(params: ProjectParams): Promise<string> {
const data = await this.request<BacklogProject>(
`/projects/${params.projectIdOrKey}`,
{},
"GET",
{
Accept: "application/json",
},
);
return JSON.stringify(data, null, 2);
}
async getIssues(params: IssuesParams): Promise<string> {
const data = await this.request<BacklogIssue[]>("/issues", params, "GET", {
Accept: "application/json",
});
return JSON.stringify(data, null, 2);
}
async getIssue(params: IssueParams): Promise<string> {
const data = await this.request<BacklogIssue>(
`/issues/${params.issueIdOrKey}`,
{},
"GET",
{
Accept: "application/json",
},
);
return JSON.stringify(data, null, 2);
}
async addIssue(params: AddIssueParams): Promise<string> {
const data = await this.request<BacklogIssue>("/issues", params, "POST", {
"Content-Type": "application/x-www-form-urlencoded",
});
return JSON.stringify(data, null, 2);
}
async updateIssue(params: UpdateIssueParams): Promise<string> {
const data = await this.request<BacklogIssue>(
`/issues/${params.issueIdOrKey}`,
{ ...params, issueIdOrKey: undefined },
"PATCH",
{
"Content-Type": "application/x-www-form-urlencoded",
},
);
return JSON.stringify(data, null, 2);
}
async deleteIssue(params: DeleteIssueParams): Promise<string> {
const data = await this.request<BacklogIssue>(
`/issues/${params.issueIdOrKey}`,
{},
"DELETE",
{
"Content-Type": "application/x-www-form-urlencoded",
},
);
return JSON.stringify(data, null, 2);
}
async getWikis(params: WikisParams): Promise<string> {
const data = await this.request<BacklogWiki[]>("/wikis", params, "GET", {
Accept: "application/json",
});
return JSON.stringify(data, null, 2);
}
async getWiki(params: WikiParams): Promise<string> {
const data = await this.request<BacklogWiki>(
`/wikis/${params.wikiId}`,
{},
"GET",
{
Accept: "application/json",
},
);
return JSON.stringify(data, null, 2);
}
async addWiki(params: AddWikiParams): Promise<string> {
const data = await this.request<BacklogWiki>("/wikis", params, "POST", {
"Content-Type": "application/x-www-form-urlencoded",
});
return JSON.stringify(data, null, 2);
}
async updateWiki(params: UpdateWikiParams): Promise<string> {
const data = await this.request<BacklogWiki>(
`/wikis/${params.wikiId}`,
{ ...params, wikiId: undefined },
"PATCH",
{
"Content-Type": "application/x-www-form-urlencoded",
},
);
return JSON.stringify(data, null, 2);
}
async deleteWiki(params: DeleteWikiParams): Promise<string> {
const data = await this.request<BacklogWiki>(
`/wikis/${params.wikiId}`,
{},
"DELETE",
{
"Content-Type": "application/x-www-form-urlencoded",
},
);
return JSON.stringify(data, null, 2);
}
}
export const backlogAPI = new BacklogAPI(BACKLOG_BASE_URL, BACKLOG_API_KEY);
```
--------------------------------------------------------------------------------
/src/core/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
const BaseParamsSchema = z.object({
offset: z
.number()
.int()
.min(0)
.optional()
.default(0)
.describe("Offset for pagination"),
count: z
.number()
.int()
.min(1)
.max(100)
.optional()
.default(10)
.describe("Number of results (1-100, default 10)"),
});
const DateRangeSchema = z.object({
createdSince: z.string().optional().describe("Start date of created date"),
createdUntil: z.string().optional().describe("End date of created date"),
updatedSince: z.string().optional().describe("Start date of updated date"),
updatedUntil: z.string().optional().describe("End date of updated date"),
startDateSince: z.string().optional().describe("Start date of start date"),
startDateUntil: z.string().optional().describe("End date of start date"),
dueDateSince: z.string().optional().describe("Start date of due date"),
dueDateUntil: z.string().optional().describe("End date of due date"),
});
const EntityIdsSchema = z.object({
projectId: z.array(z.number()).optional().describe("Project ids"),
issueTypeId: z.array(z.number()).optional().describe("Issue type ids"),
categoryId: z.array(z.number()).optional().describe("Category ids"),
versionId: z.array(z.number()).optional().describe("Version ids"),
milestoneId: z.array(z.number()).optional().describe("Milestone ids"),
statusId: z.array(z.number()).optional().describe("Status ids"),
priorityId: z.array(z.number()).optional().describe("Priority ids"),
assigneeId: z.array(z.number()).optional().describe("Assignee ids"),
createdUserId: z.array(z.number()).optional().describe("Created user ids"),
resolutionId: z.array(z.number()).optional().describe("Reason of done ids"),
id: z.array(z.number()).optional().describe("Issue ids"),
parentIssueId: z.array(z.number()).optional().describe("Parent issue ids"),
});
const ConditionSchema = z.object({
parentChild: z
.number()
.int()
.min(0)
.max(4)
.optional()
.describe(
"Condition of parent child issue. 0: All, 1: Exclude child issue, 2: Only child issue, 3: Neither parent nor child issue, 4: Only parent issue",
),
attachment: z
.boolean()
.optional()
.describe(
"Condition of attachment. true: With attachment, false: Without attachment",
),
sharedFile: z
.boolean()
.optional()
.describe(
"Condition of shared file. true: With shared file, false: Without shared file",
),
});
const SortingSchema = z.object({
sort: z
.enum([
"issueType",
"category",
"version",
"milestone",
"summary",
"status",
"priority",
"attachment",
"sharedFile",
"created",
"createdUser",
"updated",
"updatedUser",
"assignee",
"startDate",
"dueDate",
"estimatedHours",
"actualHours",
"childIssue",
])
.optional()
.describe("Attribute name for sorting"),
order: z
.enum(["asc", "desc"])
.optional()
.default("desc")
.describe("Sort order"),
});
const KeywordSchema = z.object({
keyword: z.string().optional().describe("Keyword for searching"),
});
export const ProjectsParamsSchema = z.object({
archived: z
.boolean()
.optional()
.describe(
"For unspecified parameters, this form returns all projects. For false parameters, it returns unarchived projects. For true parameters, it returns archived projects.",
),
all: z
.boolean()
.optional()
.default(false)
.describe(
"Only applies to administrators. If true, it returns all projects. If false, it returns only projects they have joined (set to false by default).",
),
});
export const ProjectParamsSchema = z.object({
projectIdOrKey: z.string().describe("Project ID or Project Key"),
});
export const IssuesParamsSchema = BaseParamsSchema.merge(DateRangeSchema)
.merge(EntityIdsSchema)
.merge(ConditionSchema)
.merge(SortingSchema)
.merge(KeywordSchema);
export const IssueParamsSchema = z.object({
issueIdOrKey: z.string().describe("Issue ID or Issue Key"),
});
export const AddIssueParamsSchema = z.object({
projectId: z.number().int().describe("Project id"),
summary: z.string().describe("Summary"),
parentIssueId: z.number().int().optional().describe("Parent issue id"),
description: z.string().optional().describe("Description"),
startDate: z.string().optional().describe("Start date"),
dueDate: z.string().optional().describe("Due date"),
estimatedHours: z.number().optional().describe("Estimated hours"),
actualHours: z.number().optional().describe("Actual hours"),
issueTypeId: z.number().int().describe("Issue type id"),
categoryId: z.array(z.number()).optional().describe("Category ids"),
versionId: z.array(z.number()).optional().describe("Version ids"),
milestoneId: z.array(z.number()).optional().describe("Milestone ids"),
priorityId: z.number().int().describe("Priority id"),
assigneeId: z.number().int().optional().describe("Assignee id"),
notifiedUserId: z.array(z.number()).optional().describe("Notified user ids"),
attachmentId: z.array(z.number()).optional().describe("Attachment ids"),
});
export const UpdateIssueParamsSchema = z.object({
issueIdOrKey: z.string().describe("Issue ID or Issue Key"),
summary: z.string().optional().describe("Summary"),
parentIssueId: z.number().int().optional().describe("Parent issue id"),
description: z.string().optional().describe("Description"),
statusId: z.number().int().optional().describe("Status id"),
startDate: z.string().optional().describe("Start date"),
dueDate: z.string().optional().describe("Due date"),
estimatedHours: z.number().optional().describe("Estimated hours"),
actualHours: z.number().optional().describe("Actual hours"),
issueTypeId: z.number().int().optional().describe("Issue type id"),
categoryId: z.array(z.number()).optional().describe("Category ids"),
versionId: z.array(z.number()).optional().describe("Version ids"),
milestoneId: z.array(z.number()).optional().describe("Milestone ids"),
priorityId: z.number().int().optional().describe("Priority id"),
assigneeId: z.number().int().optional().describe("Assignee id"),
notifiedUserId: z.array(z.number()).optional().describe("Notified user ids"),
attachmentId: z.array(z.number()).optional().describe("Attachment ids"),
comment: z.string().optional().describe("Comment"),
});
export const DeleteIssueParamsSchema = z.object({
issueIdOrKey: z.string().describe("Issue ID or Issue Key"),
});
export const WikisParamsSchema = z.object({
projectIdOrKey: z.string().describe("Project ID or Project Key"),
keyword: z.string().optional().describe("Keyword for searching"),
});
export const WikiParamsSchema = z.object({
wikiId: z.number().int().describe("Wiki page ID"),
});
export const AddWikiParamsSchema = z.object({
projectId: z.number().int().describe("Project ID"),
name: z.string().describe("Page Name"),
content: z.string().describe("Content"),
mailNotify: z.boolean().optional().describe("True make to notify by Email"),
});
export const UpdateWikiParamsSchema = z.object({
wikiId: z.number().int().describe("Wiki page ID"),
name: z.string().optional().describe("Page Name"),
content: z.string().optional().describe("Content"),
mailNotify: z.boolean().optional().describe("True make to notify by Email"),
});
export const DeleteWikiParamsSchema = z.object({
wikiId: z.number().int().describe("Wiki page ID"),
mailNotify: z.boolean().optional().describe("True make to notify by Email"),
});
export type ProjectsParams = z.infer<typeof ProjectsParamsSchema>;
export type ProjectParams = z.infer<typeof ProjectParamsSchema>;
export type IssuesParams = z.infer<typeof IssuesParamsSchema>;
export type IssueParams = z.infer<typeof IssueParamsSchema>;
export type AddIssueParams = z.infer<typeof AddIssueParamsSchema>;
export type UpdateIssueParams = z.infer<typeof UpdateIssueParamsSchema>;
export type DeleteIssueParams = z.infer<typeof DeleteIssueParamsSchema>;
export type WikisParams = z.infer<typeof WikisParamsSchema>;
export type WikiParams = z.infer<typeof WikiParamsSchema>;
export type AddWikiParams = z.infer<typeof AddWikiParamsSchema>;
export type UpdateWikiParams = z.infer<typeof UpdateWikiParamsSchema>;
export type DeleteWikiParams = z.infer<typeof DeleteWikiParamsSchema>;
```
--------------------------------------------------------------------------------
/src/tools/handlers.ts:
--------------------------------------------------------------------------------
```typescript
import {
AddIssueParamsSchema,
AddWikiParamsSchema,
DeleteIssueParamsSchema,
DeleteWikiParamsSchema,
IssueParamsSchema,
IssuesParamsSchema,
ProjectParamsSchema,
ProjectsParamsSchema,
UpdateIssueParamsSchema,
UpdateWikiParamsSchema,
WikiParamsSchema,
WikisParamsSchema,
} from "../core/schema.js";
import type { ToolName } from "../core/types.js";
import { ValidationError, formatError } from "../error/errors.js";
import {
issueService,
projectService,
wikiService,
} from "../services/index.js";
interface ToolResponse {
content: {
type: string;
text: string;
}[];
isError: boolean;
}
// biome-ignore lint/suspicious/noExplicitAny: Because the interface changes according to the purpose
type ToolHandler = (args: any) => Promise<ToolResponse>;
const handleGetProjects: ToolHandler = async (args) => {
try {
try {
const validatedParams = ProjectsParamsSchema.parse(args);
const text = await projectService.getProjects(validatedParams);
return {
content: [
{
type: "text",
text: `Results for your query:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleGetProject: ToolHandler = async (args) => {
try {
try {
const validatedParams = ProjectParamsSchema.parse(args);
const text = await projectService.getProject(validatedParams);
return {
content: [
{
type: "text",
text: `Project details for ${validatedParams.projectIdOrKey}:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleGetIssues: ToolHandler = async (args) => {
try {
try {
const validatedParams = IssuesParamsSchema.parse(args);
const text = await issueService.getIssues(validatedParams);
return {
content: [
{
type: "text",
text: `Results for your query:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleGetIssue: ToolHandler = async (args) => {
try {
try {
const validatedParams = IssueParamsSchema.parse(args);
const text = await issueService.getIssue(validatedParams);
return {
content: [
{
type: "text",
text: `Issue details for ${validatedParams.issueIdOrKey}:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleAddIssue: ToolHandler = async (args) => {
try {
try {
const validatedParams = AddIssueParamsSchema.parse(args);
const text = await issueService.addIssue(validatedParams);
return {
content: [
{
type: "text",
text: `Results for your query:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleUpdateIssue: ToolHandler = async (args) => {
try {
try {
const validatedParams = UpdateIssueParamsSchema.parse(args);
const text = await issueService.updateIssue(validatedParams);
return {
content: [
{
type: "text",
text: `Results for your query:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleDeleteIssue: ToolHandler = async (args) => {
try {
try {
const validatedParams = DeleteIssueParamsSchema.parse(args);
const text = await issueService.deleteIssue(validatedParams);
return {
content: [
{
type: "text",
text: `Results for your query:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleGetWikis: ToolHandler = async (args) => {
try {
try {
const validatedParams = WikisParamsSchema.parse(args);
const text = await wikiService.getWikis(validatedParams);
return {
content: [
{
type: "text",
text: `Results for your query:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleGetWiki: ToolHandler = async (args) => {
try {
try {
const validatedParams = WikiParamsSchema.parse(args);
const text = await wikiService.getWiki(validatedParams);
return {
content: [
{
type: "text",
text: `Wiki details for ${validatedParams.wikiId}:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleAddWiki: ToolHandler = async (args) => {
try {
try {
const validatedParams = AddWikiParamsSchema.parse(args);
const text = await wikiService.addWiki(validatedParams);
return {
content: [
{
type: "text",
text: `Results for your query:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleUpdateWiki: ToolHandler = async (args) => {
try {
try {
const validatedParams = UpdateWikiParamsSchema.parse(args);
const text = await wikiService.updateWiki(validatedParams);
return {
content: [
{
type: "text",
text: `Results for your query:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
const handleDeleteWiki: ToolHandler = async (args) => {
try {
try {
const validatedParams = DeleteWikiParamsSchema.parse(args);
const text = await wikiService.deleteWiki(validatedParams);
return {
content: [
{
type: "text",
text: `Results for your query:\n${text}`,
},
],
isError: false,
};
} catch (validationError) {
throw new ValidationError(
`Invalid parameters: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${formatError(error)}`,
},
],
isError: true,
};
}
};
export const toolHandlers: Record<ToolName, ToolHandler> = {
backlog_get_projects: handleGetProjects,
backlog_get_project: handleGetProject,
backlog_get_issues: handleGetIssues,
backlog_get_issue: handleGetIssue,
backlog_add_issue: handleAddIssue,
backlog_update_issue: handleUpdateIssue,
backlog_delete_issue: handleDeleteIssue,
backlog_get_wikis: handleGetWikis,
backlog_get_wiki: handleGetWiki,
backlog_add_wiki: handleAddWiki,
backlog_update_wiki: handleUpdateWiki,
backlog_delete_wiki: handleDeleteWiki,
};
```
--------------------------------------------------------------------------------
/src/tools/toolDefinitions.ts:
--------------------------------------------------------------------------------
```typescript
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import {
AddIssueParamsSchema,
AddWikiParamsSchema,
DeleteIssueParamsSchema,
DeleteWikiParamsSchema,
IssueParamsSchema,
IssuesParamsSchema,
ProjectParamsSchema,
ProjectsParamsSchema,
UpdateIssueParamsSchema,
UpdateWikiParamsSchema,
WikiParamsSchema,
WikisParamsSchema,
} from "../core/schema.js";
function convertZodToJsonSchema(schema: z.ZodType<unknown>) {
const isProjectsParamsSchema = schema === ProjectsParamsSchema;
const isProjectParamsSchema = schema === ProjectParamsSchema;
const isIssuesParamsSchema = schema === IssuesParamsSchema;
const isIssueParamsSchema = schema === IssueParamsSchema;
const isAddIssueParamsSchema = schema === AddIssueParamsSchema;
const isUpdateIssueParamsSchema = schema === AddIssueParamsSchema;
const isDeleteIssueParamsSchema = schema === AddIssueParamsSchema;
const isWikisParamsSchema = schema === WikisParamsSchema;
const isWikiParamsSchema = schema === WikiParamsSchema;
const isAddWikiParamsSchema = schema === AddWikiParamsSchema;
const isUpdateWikiParamsSchema = schema === UpdateWikiParamsSchema;
const isDeleteWikiParamsSchema = schema === DeleteWikiParamsSchema;
if (isProjectsParamsSchema) {
return {
type: "object" as const,
properties: {
archived: {
type: "boolean",
description:
"For unspecified parameters, this form returns all projects. " +
"For false parameters, it returns unarchived projects. " +
"For true parameters, it returns archived projects.",
},
all: {
type: "boolean",
description:
"Only applies to administrators. " +
"If true, it returns all projects. " +
"If false, it returns only projects they have joined (set to false by default).",
default: false,
},
},
};
}
if (isProjectParamsSchema) {
return {
type: "object" as const,
properties: {
projectIdOrKey: {
type: "string",
description: "Project ID or Project Key",
},
},
required: ["projectIdOrKey"],
};
}
if (isIssuesParamsSchema) {
return {
type: "object" as const,
properties: {
offset: {
type: "number",
description: "Offset for pagination",
default: 0,
},
count: {
type: "number",
description: "Number of results (1-100, default 20)",
default: 20,
minimum: 1,
maximum: 100,
},
keyword: {
type: "string",
description: "Keyword for searching",
},
sort: {
type: "string",
description: "Attribute name for sorting",
enum: [
"issueType",
"category",
"version",
"milestone",
"summary",
"status",
"priority",
"attachment",
"sharedFile",
"created",
"createdUser",
"updated",
"updatedUser",
"assignee",
"startDate",
"dueDate",
"estimatedHours",
"actualHours",
"childIssue",
],
},
order: {
type: "string",
description: "Sort order",
enum: ["asc", "desc"],
default: "desc",
},
statusId: {
type: "array",
description: "Status ids",
items: {
type: "number",
},
},
assigneeId: {
type: "array",
description: "Assignee ids",
items: {
type: "number",
},
},
createdSince: {
type: "string",
description: "Start date of created date (YYYY-MM-DD format)",
},
createdUntil: {
type: "string",
description: "End date of created date (YYYY-MM-DD format)",
},
priorityId: {
type: "array",
description: "Priority ids",
items: {
type: "number",
},
},
projectId: {
type: "array",
description: "Project ids",
items: {
type: "number",
},
},
},
};
}
if (isIssueParamsSchema) {
return {
type: "object" as const,
properties: {
issueIdOrKey: {
type: "string",
description: "Issue ID or Issue Key",
},
},
required: ["issueIdOrKey"],
};
}
if (isAddIssueParamsSchema) {
return {
type: "object" as const,
properties: {
projectId: {
type: "number",
description: "Project id",
},
summary: {
type: "string",
description: "Summary of the issue",
},
description: {
type: "string",
description: "Description of the issue",
},
issueTypeId: {
type: "number",
description: "Issue type id",
},
priorityId: {
type: "number",
description: "Priority id",
},
categoryId: {
type: "number",
description: "Category id",
},
versionId: {
type: "number",
description: "Version id",
},
milestoneId: {
type: "number",
description: "Milestone id",
},
assigneeId: {
type: "number",
description: "Assignee id",
},
startDate: {
type: "string",
description: "Start date of the issue (YYYY-MM-DD format)",
},
dueDate: {
type: "string",
description: "Due date of the issue (YYYY-MM-DD format)",
},
estimatedHours: {
type: "number",
description: "Estimated hours for the issue",
},
actualHours: {
type: "number",
description: "Actual hours for the issue",
},
},
required: ["projectId", "summary", "issueTypeId", "priorityId"],
};
}
if (isUpdateIssueParamsSchema) {
return {
type: "object" as const,
properties: {
issueIdOrKey: {
type: "string",
description: "Issue ID or Issue Key",
},
summary: {
type: "string",
description: "Summary of the issue",
},
description: {
type: "string",
description: "Description of the issue",
},
issueTypeId: {
type: "number",
description: "Issue type id",
},
priorityId: {
type: "number",
description: "Priority id",
},
categoryId: {
type: "number",
description: "Category id",
},
versionId: {
type: "number",
description: "Version id",
},
milestoneId: {
type: "number",
description: "Milestone id",
},
assigneeId: {
type: "number",
description: "Assignee id",
},
startDate: {
type: "string",
description: "Start date of the issue (YYYY-MM-DD format)",
},
dueDate: {
type: "string",
description: "Due date of the issue (YYYY-MM-DD format)",
},
estimatedHours: {
type: "number",
description: "Estimated hours for the issue",
},
actualHours: {
type: "number",
description: "Actual hours for the issue",
},
},
required: ["issueIdOrKey"],
};
}
if (isDeleteIssueParamsSchema) {
return {
type: "object" as const,
properties: {
issueIdOrKey: {
type: "string",
description: "Issue ID or Issue Key",
},
},
required: ["issueIdOrKey", "content"],
};
}
if (isWikisParamsSchema) {
return {
type: "object" as const,
properties: {
projectIdOrKey: {
type: "string",
description: "Project ID or Project Key",
},
keywords: {
type: "string",
description: "Keyword for searching",
},
},
required: ["projectIdOrKey"],
};
}
if (isWikiParamsSchema) {
return {
type: "object" as const,
properties: {
wikiId: {
type: "number",
description: "Wiki page ID",
},
},
required: ["wikiId"],
};
}
if (isAddWikiParamsSchema) {
return {
type: "object" as const,
properties: {
projectId: {
type: "number",
description: "Project ID",
},
name: {
type: "string",
description: "Page Name",
},
content: {
type: "string",
description: "Content",
},
mailNotify: {
type: "boolean",
description: "True make to notify by Email",
},
},
required: ["projectId", "name", "content"],
};
}
if (isUpdateWikiParamsSchema) {
return {
type: "object" as const,
properties: {
wikiId: {
type: "number",
description: "Wiki page ID",
},
name: {
type: "string",
description: "Page Name",
},
content: {
type: "string",
description: "Content",
},
mailNotify: {
type: "boolean",
description: "True make to notify by Email",
},
},
required: ["wikiId"],
};
}
if (isDeleteWikiParamsSchema) {
return {
type: "object" as const,
properties: {
wikiId: {
type: "number",
description: "Wiki page ID",
},
mailNotify: {
type: "boolean",
description: "True make to notify by Email",
},
},
required: ["wikiId"],
};
}
const properties: Record<string, unknown> = {};
const required: string[] = [];
if (schema instanceof z.ZodObject) {
try {
const shape = (schema as z.ZodObject<z.ZodRawShape>).shape;
if (shape && typeof shape === "object") {
for (const [key, value] of Object.entries(shape)) {
const fieldSchema = value as z.ZodTypeAny;
const propertySchema: Record<string, unknown> = { type: "string" };
// Try to extract description
const description = (fieldSchema as z.ZodType<unknown>)._def
?.description;
if (description) {
propertySchema.description = description;
}
// Check if required
if (!(fieldSchema instanceof z.ZodOptional)) {
required.push(key);
}
// Extract type information
if (
fieldSchema instanceof z.ZodNumber ||
(fieldSchema instanceof z.ZodOptional &&
fieldSchema._def.innerType instanceof z.ZodNumber)
) {
propertySchema.type = "number";
} else if (
fieldSchema instanceof z.ZodBoolean ||
(fieldSchema instanceof z.ZodOptional &&
fieldSchema._def.innerType instanceof z.ZodBoolean)
) {
propertySchema.type = "boolean";
} else if (
fieldSchema instanceof z.ZodArray ||
(fieldSchema instanceof z.ZodOptional &&
fieldSchema._def.innerType instanceof z.ZodArray)
) {
propertySchema.type = "array";
propertySchema.items = { type: "string" };
// Try to infer array item type
const arraySchema =
fieldSchema instanceof z.ZodOptional
? fieldSchema._def.innerType
: fieldSchema;
const itemType = (arraySchema as z.ZodArray<z.ZodTypeAny>)._def
?.type;
if (itemType instanceof z.ZodNumber) {
propertySchema.items = { type: "number" };
} else if (itemType instanceof z.ZodBoolean) {
propertySchema.items = { type: "boolean" };
}
} else if (
fieldSchema instanceof z.ZodEnum ||
(fieldSchema instanceof z.ZodOptional &&
fieldSchema._def.innerType instanceof z.ZodEnum)
) {
propertySchema.type = "string";
const enumSchema =
fieldSchema instanceof z.ZodOptional
? fieldSchema._def.innerType
: fieldSchema;
propertySchema.enum =
(enumSchema as z.ZodEnum<[string, ...string[]]>)._def?.values ||
[];
}
// Extract default value
if (fieldSchema instanceof z.ZodDefault) {
const defaultValue = (
fieldSchema as z.ZodDefault<z.ZodTypeAny>
)._def?.defaultValue?.();
if (defaultValue !== undefined) {
propertySchema.default = defaultValue;
}
} else if (
fieldSchema instanceof z.ZodOptional &&
fieldSchema._def.innerType instanceof z.ZodDefault
) {
const defaultValue = (
fieldSchema._def.innerType as z.ZodDefault<z.ZodTypeAny>
)._def?.defaultValue?.();
if (defaultValue !== undefined) {
propertySchema.default = defaultValue;
}
}
properties[key] = propertySchema;
}
}
} catch (e) {
console.warn("Failed to extract schema properties:", e);
}
}
return {
type: "object" as const,
properties,
...(required.length > 0 ? { required } : {}),
};
}
const createTool = (
name: string,
description: string,
// biome-ignore lint/suspicious/noExplicitAny: Because the interface changes according to the purpose
schema: z.ZodType<any>,
): Tool => {
const inputSchema = convertZodToJsonSchema(schema);
return {
name,
description,
inputSchema,
};
};
export const PROJECTS_TOOL: Tool = createTool(
"backlog_get_projects",
"Performs list project get using the Backlog Projects get API. " +
"Supports pagination, content filtering. " +
"Maximum 20 results per request, with offset for pagination.",
ProjectsParamsSchema,
);
export const PROJECT_TOOL: Tool = createTool(
"backlog_get_project",
"Performs an project get using the Backlog Project get API.",
ProjectParamsSchema,
);
export const ISSUES_TOOL: Tool = createTool(
"backlog_get_issues",
"Performs list issue get using the Backlog Issues API. " +
"Supports pagination, content filtering. " +
"Maximum 20 results per request, with offset for pagination.",
IssuesParamsSchema,
);
export const ISSUE_TOOL: Tool = createTool(
"backlog_get_issue",
"Performs an issue get using the Backlog Issue API.",
IssueParamsSchema,
);
export const ADD_ISSUE_TOOL: Tool = createTool(
"backlog_add_issue",
"Add an issue using the Backlog Issue API.",
AddIssueParamsSchema,
);
export const UPDATE_ISSUE_TOOL: Tool = createTool(
"backlog_update_issue",
"Update an issue using the Backlog Issue API.",
UpdateIssueParamsSchema,
);
export const DELETE_ISSUE_TOOL: Tool = createTool(
"backlog_delete_issue",
"Delete an issue using the Backlog Issue API.",
DeleteIssueParamsSchema,
);
export const WIKIS_TOOL: Tool = createTool(
"backlog_get_wikis",
"Performs list wikis get using the Backlog Wiki API",
WikisParamsSchema,
);
export const WIKI_TOOL: Tool = createTool(
"backlog_get_wiki",
"Performs an wiki get using the Backlog Wiki API.",
WikiParamsSchema,
);
export const ADD_WIKI_TOOL: Tool = createTool(
"backlog_add_wiki",
"Add an wiki using the Backlog Wiki API.",
AddWikiParamsSchema,
);
export const UPDATE_WIKI_TOOL: Tool = createTool(
"backlog_update_wiki",
"Update an wiki using the Backlog Wiki API.",
UpdateWikiParamsSchema,
);
export const DELETE_WIKI_TOOL: Tool = createTool(
"backlog_delete_wiki",
"Delete an wiki using the Backlog Wiki API.",
DeleteWikiParamsSchema,
);
export const ALL_TOOLS: Tool[] = [
PROJECTS_TOOL,
PROJECT_TOOL,
ISSUES_TOOL,
ISSUE_TOOL,
ADD_ISSUE_TOOL,
UPDATE_ISSUE_TOOL,
DELETE_ISSUE_TOOL,
WIKIS_TOOL,
WIKI_TOOL,
ADD_WIKI_TOOL,
UPDATE_WIKI_TOOL,
DELETE_WIKI_TOOL,
];
```