diff --git a/package-lock.json b/package-lock.json index f2a6424..11845d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "release-it": "^19.0.0", "ts-jest": "^29.3.2", "ts-node": "^10.9.2", + "tsx": "^4.20.4", "typescript": "^5.8.3" } }, @@ -566,6 +567,422 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", @@ -3649,6 +4066,47 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4529,6 +4987,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/get-uri": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", @@ -7530,6 +8000,15 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -8376,6 +8855,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true }, + "node_modules/tsx": { + "version": "4.20.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.4.tgz", + "integrity": "sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==", + "dev": true, + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 939d0af..1ea231c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "license": "MIT", "scripts": { - "dev": "NODE_ENV=development node --loader ts-node/esm src/index.ts", + "dev": "NODE_ENV=development tsx watch src/index.ts", "prebuild": "node scripts/replace-version.js", "build": "tsc && chmod 755 build/index.js", "test": "NODE_OPTIONS=--experimental-vm-modules jest", @@ -50,6 +50,7 @@ "release-it": "^19.0.0", "ts-jest": "^29.3.2", "ts-node": "^10.9.2", + "tsx": "^4.20.4", "typescript": "^5.8.3" } } diff --git a/src/backlog/backlogErrorHandler.ts b/src/backlog/backlogErrorHandler.ts index a7ba732..9d943f0 100644 --- a/src/backlog/backlogErrorHandler.ts +++ b/src/backlog/backlogErrorHandler.ts @@ -1,7 +1,17 @@ +import { ProjectAccessForbiddenError } from '../errors/ProjectAccessForbiddenError.js'; import { ErrorLike } from '../types/result.js'; import { parseBacklogAPIError } from './parseBacklogAPIError.js'; export const backlogErrorHandler = (err: unknown): ErrorLike => { + if (err instanceof ProjectAccessForbiddenError) { + return { + kind: 'error', + message: err.message, + code: err.code, + data: err.data, + }; + } + const parsed = parseBacklogAPIError(err); return { kind: 'error', diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..0cf11d6 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2025 Nulab inc. +// Licensed under the MIT License. + +import dotenv from 'dotenv'; +import { default as env } from 'env-var'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { VERSION } from './version.js'; + +dotenv.config(); + +// Define Read Guard policies +const READ_GUARD_POLICIES = ['off', 'filter', 'deny'] as const; +export type ReadGuardPolicy = (typeof READ_GUARD_POLICIES)[number]; + +// Define Write Guard policies +const WRITE_GUARD_POLICIES = ['on', 'off'] as const; +export type WriteGuardPolicy = (typeof WRITE_GUARD_POLICIES)[number]; + +export const config = yargs(hideBin(process.argv)) + .option('backlog-domain', { + type: 'string', + describe: 'Backlog domain', + default: env.get('BACKLOG_DOMAIN').required().asString(), + }) + .option('backlog-api-key', { + type: 'string', + describe: 'Backlog API key', + default: env.get('BACKLOG_API_KEY').required().asString(), + }) + .option('max-tokens', { + type: 'number', + describe: 'Maximum number of tokens allowed in the response', + default: env.get('MAX_TOKENS').default('50000').asIntPositive(), + }) + .option('optimize-response', { + type: 'boolean', + describe: + 'Enable GraphQL-style response optimization to include only requested fields', + default: env.get('OPTIMIZE_RESPONSE').default('false').asBool(), + }) + .option('prefix', { + type: 'string', + describe: 'Optional string prefix to prepend to all generated outputs', + default: env.get('PREFIX').default('').asString(), + }) + .option('export-translations', { + type: 'boolean', + describe: 'Export translations and exit', + default: false, + }) + .option('enable-toolsets', { + type: 'array', + describe: `Specify which toolsets to enable. Defaults to 'all'.`, + default: env.get('ENABLE_TOOLSETS').default('all').asArray(','), + }) + .option('dynamic-toolsets', { + type: 'boolean', + describe: + 'Enable dynamic toolsets such as enable_toolset, list_available_toolsets, etc.', + default: env.get('ENABLE_DYNAMIC_TOOLSETS').default('false').asBool(), + }) + .option('allowed-project-ids', { + type: 'array', + describe: 'Comma-separated list of allowed Backlog project IDs', + default: env.get('BACKLOG_ALLOWED_PROJECT_IDS').default('').asArray(',').filter(Boolean), + }) + .option('allowed-project-keys', { + type: 'array', + describe: 'Comma-separated list of allowed Backlog project keys', + default: env.get('BACKLOG_ALLOWED_PROJECT_KEYS').default('').asArray(',').filter(Boolean), + }) + .option('key-resolve-ttl-sec', { + type: 'number', + describe: 'Cache TTL in seconds for project key-to-ID resolution', + default: env.get('BACKLOG_KEY_RESOLVE_TTL_SEC').default(300).asInt(), + }) + .version(VERSION) + .help() + .alias('h', 'help') + .parseSync(); diff --git a/src/errors/ProjectAccessForbiddenError.ts b/src/errors/ProjectAccessForbiddenError.ts new file mode 100644 index 0000000..7b957c7 --- /dev/null +++ b/src/errors/ProjectAccessForbiddenError.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2025 Nulab inc. +// Licensed under the MIT License. + +import { ReadGuardPolicy, WriteGuardPolicy } from '../config.js'; + +export interface ProjectAccessForbiddenErrorData { + allowedProjectIds: (string | number)[]; + requestedProjectId?: number; + requestedProjectKey?: string; + requestedProjectIds?: number[]; +} + +export class ProjectAccessForbiddenError extends Error { + public readonly code = -32040; + public readonly data: ProjectAccessForbiddenErrorData; + + constructor(message: string, data: ProjectAccessForbiddenErrorData) { + super(message); + this.name = 'ProjectAccessForbiddenError'; + this.data = data; + } +} diff --git a/src/guards/ProjectGuardService.test.ts b/src/guards/ProjectGuardService.test.ts new file mode 100644 index 0000000..c9b85f0 --- /dev/null +++ b/src/guards/ProjectGuardService.test.ts @@ -0,0 +1,131 @@ +// Copyright (c) 2025 Nulab inc. +// Licensed under the MIT License. + +import { jest } from '@jest/globals'; +import { Backlog } from 'backlog-js'; +import { ProjectGuardService } from './ProjectGuardService.js'; +import { logger } from '../utils/logger.js'; + +const mockBacklog = { + getProjects: jest.fn(), + getProject: jest.fn(), +} as unknown as Backlog; + +describe('ProjectGuardService', () => { + beforeEach(() => { + jest.clearAllMocks(); + (mockBacklog.getProjects as jest.Mock).mockResolvedValue([ + { id: 1, projectKey: 'HOME' }, + { id: 2, projectKey: 'TEST-1' }, + ]); + jest.spyOn(logger, 'warn').mockImplementation(() => {}); + }); + + describe('Initialization and Validation', () => { + it('should initialize with allowed project IDs', async () => { + const service = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [123, 456], + allowedProjectKeys: [], + writeGuard: 'on', + readGuard: 'filter', + keyResolveTtlSec: 300, + }); + await service.initialize(); + expect(service.getAllowedProjectIds()).toEqual(new Set([123, 456])); + }); + + it('should resolve and add allowed project keys', async () => { + const service = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [123], + allowedProjectKeys: ['HOME'], + writeGuard: 'on', + readGuard: 'filter', + keyResolveTtlSec: 300, + }); + await service.initialize(); + expect(service.getAllowedProjectIds()).toEqual(new Set([123, 1])); + }); + + it('should throw if a project key cannot be resolved', async () => { + const service = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [], + allowedProjectKeys: ['UNKNOWN'], + writeGuard: 'on', + readGuard: 'filter', + keyResolveTtlSec: 300, + }); + await expect(service.initialize()).rejects.toThrow( + 'Failed to resolve project key: UNKNOWN' + ); + }); + + it('should throw if guards are on but no projects are allowed', async () => { + const service = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [], + allowedProjectKeys: [], + writeGuard: 'on', + readGuard: 'off', + keyResolveTtlSec: 300, + }); + await expect(service.initialize()).rejects.toThrow( + 'FATAL: Guards are enabled but no allowed projects are configured.' + ); + }); + + it('should warn in dev if projects are allowed but guards are off', async () => { + process.env.NODE_ENV = 'development'; + const service = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [1], + allowedProjectKeys: [], + writeGuard: 'off', + readGuard: 'off', + keyResolveTtlSec: 300, + }); + await service.initialize(); + expect(logger.warn).toHaveBeenCalledWith( + 'WARNING: Allowed projects are configured but both read and write guards are off.' + ); + }); + + it('should throw in prod if projects are allowed but guards are off', async () => { + process.env.NODE_ENV = 'production'; + const service = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [1], + allowedProjectKeys: [], + writeGuard: 'off', + readGuard: 'off', + keyResolveTtlSec: 300, + }); + await expect(service.initialize()).rejects.toThrow( + 'FATAL: WARNING: Allowed projects are configured but both read and write guards are off.' + ); + }); + + it('should throw in prod if fully unguarded without UNGUARDED_OK flag', async () => { + process.env.NODE_ENV = 'production'; + const service = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [], + allowedProjectKeys: [], + writeGuard: 'off', + readGuard: 'off', + keyResolveTtlSec: 300, + }); + await expect(service.initialize()).rejects.toThrow( + 'FATAL: Running in production without guards requires BACKLOG_UNGUARDED_OK=I_UNDERSTAND_THE_RISKS' + ); + }); + + it('should not throw in prod if fully unguarded with UNGUARDED_OK flag', async () => { + process.env.NODE_ENV = 'production'; + const service = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [], + allowedProjectKeys: [], + writeGuard: 'off', + readGuard: 'off', + unguardedOk: 'I_UNDERSTAND_THE_RISKS', + keyResolveTtlSec: 300, + }); + await expect(service.initialize()).resolves.not.toThrow(); + }); + }); +}); diff --git a/src/guards/ProjectGuardService.ts b/src/guards/ProjectGuardService.ts new file mode 100644 index 0000000..27dbe71 --- /dev/null +++ b/src/guards/ProjectGuardService.ts @@ -0,0 +1,68 @@ +import { Backlog } from 'backlog-js'; + +type ProjectGuardConfig = { + allowedProjectIds: (string | number)[]; + allowedProjectKeys: string[]; + keyResolveTtlSec: number; +}; + +export class ProjectGuardService { + private readonly allowedProjectIds = new Set(); + private readonly allowedProjectKeys = new Set(); + private readonly config: ProjectGuardConfig; + private readonly backlog: Backlog; + + constructor(backlog: Backlog, config: ProjectGuardConfig) { + this.backlog = backlog; + this.config = config; + } + + async initialize(): Promise { + this.config.allowedProjectIds.forEach((id) => { + const numericId = Number(id); + if (!isNaN(numericId)) { + this.allowedProjectIds.add(numericId); + } + }); + + if (this.config.allowedProjectKeys.length > 0) { + // Todo: add TTL caching and try catch errors + const projects = await this.backlog.getProjects(); + const projectMap = new Map(); + projects.forEach((p: any) => projectMap.set(p.projectKey, p.id)); + + for (const key of this.config.allowedProjectKeys) { + const id = projectMap.get(key); + if (id) { + this.allowedProjectKeys.add(key); + this.allowedProjectIds.add(id); + } else { + throw new Error(`Failed to resolve project key: ${key}`); + } + } + } + } + + public isAllowed(projectId: number): boolean; + public isAllowed(projectKey: string): boolean; + public isAllowed(project: number | string): boolean { + if (typeof project === 'number') { + return ( + this.allowedProjectIds.size === 0 || this.allowedProjectIds.has(project) + ); + } + + if (typeof project === 'string') { + if (this.allowedProjectIds.size === 0) { + return true; + } + return this.allowedProjectKeys.has(project); + } + + return false; + } + + public getAllowedProjectIds(): Set { + return this.allowedProjectIds; + } +} diff --git a/src/handlers/transformers/wrapWithProjectGuard.test.ts b/src/handlers/transformers/wrapWithProjectGuard.test.ts new file mode 100644 index 0000000..3706bb4 --- /dev/null +++ b/src/handlers/transformers/wrapWithProjectGuard.test.ts @@ -0,0 +1,140 @@ +// Copyright (c) 2025 Nulab inc. +// Licensed under the MIT License. + +import { jest } from '@jest/globals'; +import { Backlog } from 'backlog-js'; +import { ProjectGuardService } from '../../guards/ProjectGuardService.js'; +import { wrapWithProjectGuard } from './wrapWithProjectGuard.js'; +import { ProjectAccessForbiddenError } from '../../errors/ProjectAccessForbiddenError.js'; + +const mockBacklog: any = { + getProject: jest.fn(), +}; + +const mockHandler: any = jest.fn(); + +describe('wrapWithProjectGuard', () => { + let guardService: ProjectGuardService; + + beforeEach(() => { + jest.clearAllMocks(); + mockHandler.mockResolvedValue({ success: true }); + mockBacklog.getProject.mockImplementation(async (key: string | number) => { + if (key === 'ALLOWED') return { id: 1 }; + if (key === 'DISALLOWED') return { id: 999 }; + if (key === 1) return { id: 1 }; + if (key === 999) return { id: 999 }; + return { id: 2 }; + }); + }); + + // --- WRITE GUARD TESTS --- + describe('Write Guard (on)', () => { + beforeEach(async () => { + guardService = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [1, 2], + allowedProjectKeys: [], + writeGuard: 'on', + readGuard: 'off', + keyResolveTtlSec: 0, + } as any); + await guardService.initialize(); + }); + + it('should allow write with an allowed projectId', async () => { + const wrapped = wrapWithProjectGuard(mockHandler, 'addIssue', guardService, mockBacklog); + await expect(wrapped({ projectId: 1 })).resolves.toEqual({ success: true }); + }); + + it('should block write with a disallowed projectId', async () => { + const wrapped = wrapWithProjectGuard(mockHandler, 'addIssue', guardService, mockBacklog); + await expect(wrapped({ projectId: 999 })).rejects.toThrow(ProjectAccessForbiddenError); + }); + + it('should resolve and allow write with an allowed projectKey', async () => { + const wrapped = wrapWithProjectGuard(mockHandler, 'addIssue', guardService, mockBacklog); + await expect(wrapped({ projectKey: 'ALLOWED' })).resolves.toEqual({ success: true }); + }); + + it('should allow write when projectIds array includes an allowed project', async () => { + const wrapped = wrapWithProjectGuard(mockHandler, 'addIssue', guardService, mockBacklog); + const params: any = { projectIds: [999, 1, 888] }; + await expect(wrapped(params)).resolves.toEqual({ success: true }); + expect(params.projectId).toBe(1); + }); + + it('should block write when projectIds array has no allowed projects', async () => { + const wrapped = wrapWithProjectGuard(mockHandler, 'addIssue', guardService, mockBacklog); + await expect(wrapped({ projectIds: [999, 888] })).rejects.toThrow( + ProjectAccessForbiddenError + ); + }); + + it('should use default project ID if none is provided', async () => { + guardService = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [1, 2], + allowedProjectKeys: [], + writeGuard: 'on', + readGuard: 'off', + defaultProjectId: 2, + keyResolveTtlSec: 0, + } as any); + const wrapped = wrapWithProjectGuard(mockHandler, 'addIssue', guardService, mockBacklog); + const params = {}; + await wrapped(params); + expect(params).toEqual({ projectId: 2 }); + }); + }); + + // --- READ GUARD (deny) TESTS --- + describe('Read Guard (deny)', () => { + beforeEach(async () => { + guardService = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [1, 2], + allowedProjectKeys: [], + writeGuard: 'off', + readGuard: 'deny', + keyResolveTtlSec: 0, + } as any); + await guardService.initialize(); + }); + + it('should deny read from a disallowed project', async () => { + const wrapped = wrapWithProjectGuard(mockHandler, 'getIssue', guardService, mockBacklog); + await expect(wrapped({ projectId: 999 })).rejects.toThrow(ProjectAccessForbiddenError); + }); + + it('should throw if no project is specified and multiple are allowed', async () => { + const wrapped = wrapWithProjectGuard(mockHandler, 'getIssues', guardService, mockBacklog); + await expect(wrapped({})).rejects.toThrow('Project must be specified'); + }); + }); + + // --- READ GUARD (filter) TESTS --- + describe('Read Guard (filter)', () => { + beforeEach(async () => { + guardService = new ProjectGuardService(mockBacklog, { + allowedProjectIds: [1, 2], + allowedProjectKeys: [], + writeGuard: 'off', + readGuard: 'filter', + keyResolveTtlSec: 0, + } as any); + await guardService.initialize(); + }); + + it('should inject all allowed project IDs if none are provided', async () => { + const wrapped = wrapWithProjectGuard(mockHandler, 'getIssues', guardService, mockBacklog); + const params = {}; + await wrapped(params); + expect(params).toEqual({ projectId: [1, 2] }); + }); + + it('should filter provided project IDs to only allowed ones', async () => { + const wrapped = wrapWithProjectGuard(mockHandler, 'getIssues', guardService, mockBacklog); + const params = { projectId: [1, 999, 2, 888] }; + await wrapped(params); + expect(params).toEqual({ projectId: [1, 2] }); + }); + }); +}); diff --git a/src/handlers/transformers/wrapWithProjectGuard.ts b/src/handlers/transformers/wrapWithProjectGuard.ts new file mode 100644 index 0000000..7208bdd --- /dev/null +++ b/src/handlers/transformers/wrapWithProjectGuard.ts @@ -0,0 +1,110 @@ +// Copyright (c) 2025 Nulab inc. +// Licensed under the MIT License. + +import { ProjectGuardService } from '../../guards/ProjectGuardService.js'; +import { + ProjectAccessForbiddenError, + ProjectAccessForbiddenErrorData, +} from '../../errors/ProjectAccessForbiddenError.js'; +import { Backlog } from 'backlog-js'; +import { logger } from '../../utils/logger.js'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ToolHandler = (params: any) => Promise; + +export const wrapWithProjectGuard = ( + handler: ToolHandler, + toolName: string, + guardService: ProjectGuardService, + backlog: Backlog +): ToolHandler => { + return async (params: any) => { + const { projectId, projectKey, projectIds } = params; + + const isNilOrEmpty = (value: unknown): boolean => { + if (value === null || value === undefined) { + return true; + } + if (typeof value === 'string') { + return value.trim().length === 0; + } + if (Array.isArray(value)) { + return value.length === 0; + } + return false; + }; + + if ( + isNilOrEmpty(projectId) && + isNilOrEmpty(projectKey) && + isNilOrEmpty(projectIds) + ) { + logger.debug( + { toolName, result: 'skipped' }, + 'Project guard skipped (no project context provided)' + ); + return handler(params); + } + + const coerceToNumericIds = (value: unknown): number[] => { + if (Array.isArray(value)) { + return value + .map((item) => Number(item)) + .filter((id) => Number.isFinite(id)); + } + const numericValue = Number(value); + return Number.isFinite(numericValue) ? [numericValue] : []; + }; + + const primaryCandidates = coerceToNumericIds(projectId); + const secondaryCandidates = coerceToNumericIds(projectIds); + + const resolveCandidate = (candidates: number[]): number[] => { + if (candidates.length === 0) { + return []; + } + const allowed = candidates.find((id) => guardService.isAllowed(id)); + return allowed ? [allowed] : [candidates[0]]; + } + + let targetProjectIds: number[] = resolveCandidate(primaryCandidates); + + if (projectKey) { + // This is a simplified resolution. A real implementation would cache. + const project = await backlog.getProject(projectKey); + targetProjectIds = [project.id]; + } + + for(const targetProjectId of targetProjectIds) { + if (!targetProjectId || !guardService.isAllowed(targetProjectId)) { + const errorData: ProjectAccessForbiddenErrorData = { + allowedProjectIds: [...guardService.getAllowedProjectIds()], + requestedProjectId: targetProjectId, + requestedProjectKey: projectKey, + requestedProjectIds: + primaryCandidates.length || secondaryCandidates.length + ? [...primaryCandidates, ...secondaryCandidates] + : undefined, + }; + logger.warn( + { toolName, result: 'blocked', ...errorData }, + 'Project write access blocked' + ); + throw new ProjectAccessForbiddenError( + 'Write operation is not allowed for this project', + errorData + ); + } + } + + logger.info( + { + toolName, + result: 'allowed', + projectIds: targetProjectIds, + }, + 'Project write access allowed' + ); + return handler(params); + }; +}; diff --git a/src/index.ts b/src/index.ts index 40b5de0..ee59b5b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,11 +5,9 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import * as backlogjs from 'backlog-js'; -import dotenv from 'dotenv'; -import { default as env } from 'env-var'; -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; +import { config } from './config.js'; import { createTranslationHelper } from './createTranslationHelper.js'; +import { ProjectGuardService } from './guards/ProjectGuardService.js'; import { registerDyamicTools, registerTools } from './registerTools.js'; import { dynamicTools } from './tools/dynamicTools/toolsets.js'; import { logger } from './utils/logger.js'; @@ -18,57 +16,12 @@ import { buildToolsetGroup } from './utils/toolsetUtils.js'; import { wrapServerWithToolRegistry } from './utils/wrapServerWithToolRegistry.js'; import { VERSION } from './version.js'; -dotenv.config(); - -const domain = env.get('BACKLOG_DOMAIN').required().asString(); - -const apiKey = env.get('BACKLOG_API_KEY').required().asString(); - -const backlog = new backlogjs.Backlog({ host: domain, apiKey: apiKey }); - -const argv = yargs(hideBin(process.argv)) - .option('max-tokens', { - type: 'number', - describe: 'Maximum number of tokens allowed in the response', - default: env.get('MAX_TOKENS').default('50000').asIntPositive(), - }) - .option('optimize-response', { - type: 'boolean', - describe: - 'Enable GraphQL-style response optimization to include only requested fields', - default: env.get('OPTIMIZE_RESPONSE').default('false').asBool(), - }) - .option('prefix', { - type: 'string', - describe: 'Optional string prefix to prepend to all generated outputs', - default: env.get('PREFIX').default('').asString(), - }) - .option('export-translations', { - type: 'boolean', - describe: 'Export translations and exit', - default: false, - }) - .option('enable-toolsets', { - type: 'array', - describe: `Specify which toolsets to enable. Defaults to 'all'. -Available toolsets: - - space: Tools for managing Backlog space settings and general information - - project: Tools for managing projects, categories, custom fields, and issue types - - issue: Tools for managing issues and their comments - - wiki: Tools for managing wiki pages - - git: Tools for managing Git repositories and pull requests - - notifications: Tools for managing user notifications`, - default: env.get('ENABLE_TOOLSETS').default('all').asArray(','), - }) - .option('dynamic-toolsets', { - type: 'boolean', - describe: - 'Enable dynamic toolsets such as enable_toolset, list_available_toolsets, etc.', - default: env.get('ENABLE_DYNAMIC_TOOLSETS').default('false').asBool(), - }) - .parseSync(); +const backlog = new backlogjs.Backlog({ + host: config.backlogDomain, + apiKey: config.backlogApiKey, +}); -const useFields = argv.optimizeResponse; +const useFields = config.optimizeResponse; const server = wrapServerWithToolRegistry( new McpServer({ @@ -83,38 +36,48 @@ Start with the example above and customize freely.` const transHelper = createTranslationHelper(); -const maxTokens = argv.maxTokens; -const prefix = argv.prefix; -let enabledToolsets = argv.enableToolsets as string[]; +const maxTokens = config.maxTokens; +const prefix = config.prefix; +let enabledToolsets = config.enableToolsets as string[]; // If dynamic toolsets are enabled, remove "all" to allow for selective enabling via commands -if (argv.dynamicToolsets) { +if (config.dynamicToolsets) { enabledToolsets = enabledToolsets.filter((a) => a != 'all'); } const mcpOption = { useFields: useFields, maxTokens, prefix }; const toolsetGroup = buildToolsetGroup(backlog, transHelper, enabledToolsets); -// Register all tools -registerTools(server, toolsetGroup, mcpOption); - -// Register dynamic tool management tools if enabled -if (argv.dynamicToolsets) { - const registrar = createToolRegistrar(server, toolsetGroup, mcpOption); - const dynamicToolsetGroup = dynamicTools( - registrar, - transHelper, - toolsetGroup - ); - - registerDyamicTools(server, dynamicToolsetGroup, prefix); -} - -if (argv.exportTranslations) { - const data = transHelper.dump(); - // eslint-disable-next-line no-console - console.log(JSON.stringify(data, null, 2)); - process.exit(0); +async function start() { + // Register all tools + const guardService = new ProjectGuardService(backlog, { + allowedProjectIds: config.allowedProjectIds as (string | number)[], + allowedProjectKeys: config.allowedProjectKeys as string[], + keyResolveTtlSec: config.keyResolveTtlSec, + }); + + await guardService.initialize(); + + registerTools(server, toolsetGroup, mcpOption, guardService, backlog); + + // Register dynamic tool management tools if enabled + if (config.dynamicToolsets) { + const registrar = createToolRegistrar(server, toolsetGroup, mcpOption, guardService, backlog); + const dynamicToolsetGroup = dynamicTools( + registrar, + transHelper, + toolsetGroup + ); + + registerDyamicTools(server, dynamicToolsetGroup, prefix); + } + + if (config.exportTranslations) { + const data = transHelper.dump(); + // eslint-disable-next-line no-console + console.log(JSON.stringify(data, null, 2)); + process.exit(0); + } } async function main() { @@ -123,7 +86,9 @@ async function main() { logger.info('Backlog MCP Server running on stdio'); } -main().catch((error) => { - logger.error({ err: error }, 'Fatal error in main()'); - process.exit(1); -}); +start() + .then(main) + .catch((error) => { + logger.error({ err: error }, 'Fatal error in main()'); + process.exit(1); + }); diff --git a/src/registerTools.ts b/src/registerTools.ts index aa42935..b2c8363 100644 --- a/src/registerTools.ts +++ b/src/registerTools.ts @@ -1,5 +1,8 @@ +import { Backlog } from 'backlog-js'; import { backlogErrorHandler } from './backlog/backlogErrorHandler.js'; import { composeToolHandler } from './handlers/builders/composeToolHandler.js'; +import { wrapWithProjectGuard } from './handlers/transformers/wrapWithProjectGuard.js'; +import { ProjectGuardService } from './guards/ProjectGuardService.js'; import { MCPOptions } from './types/mcp.js'; import { DynamicToolDefinition, ToolDefinition } from './types/tool.js'; import { DynamicToolsetGroup, ToolsetGroup } from './types/toolsets.js'; @@ -22,7 +25,9 @@ type RegisterOptions = { export function registerTools( server: BacklogMCPServer, toolsetGroup: ToolsetGroup, - options: MCPOptions + options: MCPOptions, + guardService: ProjectGuardService, + backlog: Backlog ) { const { useFields, maxTokens, prefix } = options; @@ -30,13 +35,26 @@ export function registerTools( server, toolsetGroup, prefix, - handlerStrategy: (tool) => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - composeToolHandler(tool as ToolDefinition, { - useFields, - errorHandler: backlogErrorHandler, - maxTokens, - }), + handlerStrategy: (tool) => { + const composedHandler = composeToolHandler( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + tool as ToolDefinition, + { + useFields, + errorHandler: backlogErrorHandler, + maxTokens, + } + ); + // Wrap composedHandler to accept a single argument as expected by wrapWithProjectGuard + const handlerWithSingleArg = (input: any) => + composedHandler(input, { signal: new AbortController().signal }); + return wrapWithProjectGuard( + handlerWithSingleArg, + tool.name, + guardService, + backlog + ); + }, }); } diff --git a/src/types/result.ts b/src/types/result.ts index 9c90485..965e35a 100644 --- a/src/types/result.ts +++ b/src/types/result.ts @@ -1,6 +1,8 @@ export type ErrorLike = { kind: 'error'; message: string; + code?: number; + data?: unknown; }; export type Success = { diff --git a/src/utils/toolRegistrar.ts b/src/utils/toolRegistrar.ts index d5575b7..35959ae 100644 --- a/src/utils/toolRegistrar.ts +++ b/src/utils/toolRegistrar.ts @@ -1,3 +1,5 @@ +import { Backlog } from 'backlog-js'; +import { ProjectGuardService } from '../guards/ProjectGuardService.js'; import { registerTools } from '../registerTools.js'; import { MCPOptions } from '../types/mcp.js'; import { ToolRegistrar } from '../types/tool.js'; @@ -8,12 +10,14 @@ import { BacklogMCPServer } from './wrapServerWithToolRegistry.js'; export function createToolRegistrar( server: BacklogMCPServer, toolsetGroup: ToolsetGroup, - options: MCPOptions + options: MCPOptions, + guardService: ProjectGuardService, + backlog: Backlog ): ToolRegistrar { return { async enableToolsetAndRefresh(toolset: string): Promise { const msg = enableToolset(toolsetGroup, toolset); - registerTools(server, toolsetGroup, options); + registerTools(server, toolsetGroup, options, guardService, backlog); await server.server.sendToolListChanged(); return msg; },