backlog-mcp-server 0.20.0 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -287,7 +287,6 @@ Tools for managing projects, categories, custom fields, and issue types.
287
287
  - `get_project`: Returns information about a specific project.
288
288
  - `get_project_users`: Returns list of users in a specific project.
289
289
  - `update_project`: Updates an existing project.
290
- - `delete_project`: Deletes a project.
291
290
 
292
291
  ### Toolset: `issue`
293
292
 
@@ -308,6 +307,7 @@ Tools for managing issues, their comments, and related items like priorities, ca
308
307
  - `remove_related_issue`: Removes the relation between an issue and a related issue.
309
308
  - `get_priorities`: Returns list of priorities.
310
309
  - `get_categories`: Returns list of categories for a project.
310
+ - `add_category`: Creates a new category for a project.
311
311
  - `get_custom_fields`: Returns list of custom fields for a project.
312
312
  - `get_issue_types`: Returns list of issue types for a project.
313
313
  - `get_resolutions`: Returns list of issue resolutions.
@@ -1,2 +1,20 @@
1
- export declare function runWithAccessToken<T>(token: string | undefined, fn: () => Promise<T>): Promise<T>;
1
+ /**
2
+ * Runs `fn` with the OAuth access token of the current request in scope.
3
+ *
4
+ * `onAuthError` is invoked the first time a Backlog call inside `fn` is
5
+ * rejected with an authentication error. It runs at the moment of detection
6
+ * rather than after `fn` settles, because a response that has already upgraded
7
+ * to SSE resolves before its handler finishes — the caller cannot rely on
8
+ * inspecting the outcome afterwards to invalidate anything.
9
+ */
10
+ export declare function runWithAccessToken<T>(token: string, fn: () => Promise<T>, onAuthError?: () => void): Promise<T>;
2
11
  export declare function getCurrentAccessToken(): string | undefined;
12
+ /**
13
+ * Reports that Backlog rejected the credentials of the current request.
14
+ *
15
+ * A no-op outside {@link runWithAccessToken}, which is how API-key mode stays
16
+ * unaffected: nothing establishes this context on the stdio transport.
17
+ */
18
+ export declare function reportBacklogAuthError(): void;
19
+ /** Whether {@link reportBacklogAuthError} was called in the current context. */
20
+ export declare function hasBacklogAuthErrorBeenReported(): boolean;
@@ -1,10 +1,36 @@
1
1
  // Copyright (c) 2025 Nulab inc.
2
2
  // Licensed under the MIT License.
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
- const accessTokenStorage = new AsyncLocalStorage();
5
- export function runWithAccessToken(token, fn) {
6
- return accessTokenStorage.run(token, fn);
4
+ const authContextStorage = new AsyncLocalStorage();
5
+ /**
6
+ * Runs `fn` with the OAuth access token of the current request in scope.
7
+ *
8
+ * `onAuthError` is invoked the first time a Backlog call inside `fn` is
9
+ * rejected with an authentication error. It runs at the moment of detection
10
+ * rather than after `fn` settles, because a response that has already upgraded
11
+ * to SSE resolves before its handler finishes — the caller cannot rely on
12
+ * inspecting the outcome afterwards to invalidate anything.
13
+ */
14
+ export function runWithAccessToken(token, fn, onAuthError) {
15
+ return authContextStorage.run({ accessToken: token, onAuthError, authErrorReported: false }, fn);
7
16
  }
8
17
  export function getCurrentAccessToken() {
9
- return accessTokenStorage.getStore();
18
+ return authContextStorage.getStore()?.accessToken;
19
+ }
20
+ /**
21
+ * Reports that Backlog rejected the credentials of the current request.
22
+ *
23
+ * A no-op outside {@link runWithAccessToken}, which is how API-key mode stays
24
+ * unaffected: nothing establishes this context on the stdio transport.
25
+ */
26
+ export function reportBacklogAuthError() {
27
+ const context = authContextStorage.getStore();
28
+ if (!context || context.authErrorReported)
29
+ return;
30
+ context.authErrorReported = true;
31
+ context.onAuthError?.();
32
+ }
33
+ /** Whether {@link reportBacklogAuthError} was called in the current context. */
34
+ export function hasBacklogAuthErrorBeenReported() {
35
+ return authContextStorage.getStore()?.authErrorReported ?? false;
10
36
  }
@@ -1,15 +1,23 @@
1
1
  // Copyright (c) 2025 Nulab inc.
2
2
  // Licensed under the MIT License.
3
3
  import { verifyBacklogToken } from './backlogOAuthClient.js';
4
+ import { hasBacklogAuthErrorBeenReported, runWithAccessToken, } from './backlogAuthContext.js';
4
5
  import { logger } from '../utils/logger.js';
5
6
  const CACHE_TTL_MS = 5 * 60 * 1000;
6
7
  export function createBearerAuthMiddleware(store, config, mcpPath) {
7
8
  const prmPath = mcpPath === '/' ? '' : mcpPath;
8
9
  const resourceMetadataUrl = `${config.serverBaseUrl}/.well-known/oauth-protected-resource${prmPath}`;
10
+ const wwwAuthenticate = (description) => description
11
+ ? `Bearer error="invalid_token", error_description="${description}", resource_metadata="${resourceMetadataUrl}"`
12
+ : `Bearer resource_metadata="${resourceMetadataUrl}"`;
9
13
  return async (c, next) => {
14
+ const unauthorized = (description, header = description) => {
15
+ c.header('WWW-Authenticate', wwwAuthenticate(header));
16
+ return c.json({ error: 'invalid_token', error_description: description }, 401);
17
+ };
10
18
  const authHeader = c.req.header('authorization');
11
19
  if (!authHeader) {
12
- c.header('WWW-Authenticate', `Bearer resource_metadata="${resourceMetadataUrl}"`);
20
+ c.header('WWW-Authenticate', wwwAuthenticate());
13
21
  return c.json({
14
22
  error: 'invalid_token',
15
23
  error_description: 'Missing Authorization header',
@@ -17,42 +25,49 @@ export function createBearerAuthMiddleware(store, config, mcpPath) {
17
25
  }
18
26
  const [type, mcpToken] = authHeader.split(' ');
19
27
  if (type?.toLowerCase() !== 'bearer' || !mcpToken) {
20
- c.header('WWW-Authenticate', `Bearer error="invalid_token", error_description="Invalid Authorization header format", resource_metadata="${resourceMetadataUrl}"`);
21
- return c.json({ error: 'invalid_token', error_description: 'Expected Bearer token' }, 401);
28
+ return unauthorized('Expected Bearer token', 'Invalid Authorization header format');
22
29
  }
23
30
  const tokenEntry = store.getMcpToken(mcpToken);
24
31
  if (!tokenEntry) {
25
- c.header('WWW-Authenticate', `Bearer error="invalid_token", error_description="Unknown or expired token", resource_metadata="${resourceMetadataUrl}"`);
26
- return c.json({
27
- error: 'invalid_token',
28
- error_description: 'Unknown or expired token',
29
- }, 401);
32
+ return unauthorized('Unknown or expired token');
30
33
  }
31
- const cached = store.getCachedVerification(mcpToken);
32
- if (cached) {
33
- c.set('authInfo', cached);
34
- await next();
35
- return;
34
+ let authInfo = store.getCachedVerification(mcpToken);
35
+ if (!authInfo) {
36
+ try {
37
+ const user = await verifyBacklogToken(config.backlogDomain, tokenEntry.backlogAccessToken);
38
+ authInfo = {
39
+ token: tokenEntry.backlogAccessToken,
40
+ clientId: String(user.id),
41
+ scopes: [],
42
+ expiresAt: Math.floor(Date.now() / 1000) + CACHE_TTL_MS / 1000,
43
+ };
44
+ store.cacheVerification(mcpToken, authInfo, CACHE_TTL_MS);
45
+ }
46
+ catch (err) {
47
+ logger.warn({ err }, 'Bearer token verification failed');
48
+ return unauthorized('Token verification failed');
49
+ }
36
50
  }
37
- try {
38
- const user = await verifyBacklogToken(config.backlogDomain, tokenEntry.backlogAccessToken);
39
- const authInfo = {
40
- token: tokenEntry.backlogAccessToken,
41
- clientId: String(user.id),
42
- scopes: [],
43
- expiresAt: Math.floor(Date.now() / 1000) + CACHE_TTL_MS / 1000,
44
- };
45
- store.cacheVerification(mcpToken, authInfo, CACHE_TTL_MS);
46
- c.set('authInfo', authInfo);
51
+ c.set('authInfo', authInfo);
52
+ // The stored token is the only credential a downstream Backlog call has, so
53
+ // Backlog rejecting it is an authentication event and not a tool failure.
54
+ // Dropping the entry here is what lets the client recover: the next request
55
+ // fails the `getMcpToken` check above and is told to re-authenticate.
56
+ //
57
+ // Invalidation happens the moment the failure is reported rather than after
58
+ // `next()` settles, because a response that upgraded to SSE resolves before
59
+ // its handler has run.
60
+ const onAuthError = () => {
61
+ logger.warn({ clientId: tokenEntry.clientId }, 'Backlog rejected the stored access token; revoking the MCP token');
62
+ store.revokeMcpToken(mcpToken);
63
+ };
64
+ await runWithAccessToken(tokenEntry.backlogAccessToken, async () => {
47
65
  await next();
48
- }
49
- catch (err) {
50
- logger.warn({ err }, 'Bearer token verification failed');
51
- c.header('WWW-Authenticate', `Bearer error="invalid_token", error_description="Token verification failed", resource_metadata="${resourceMetadataUrl}"`);
52
- return c.json({
53
- error: 'invalid_token',
54
- error_description: 'Token verification failed',
55
- }, 401);
56
- }
66
+ // Read inside the context: `next()` resolving is not the end of the
67
+ // async scope, but it is the point at which the response is decided.
68
+ if (hasBacklogAuthErrorBeenReported()) {
69
+ c.res = unauthorized('Backlog rejected the access token');
70
+ }
71
+ }, onAuthError);
57
72
  };
58
73
  }
@@ -6,6 +6,8 @@ import { buildBacklogAuthorizationUrl, exchangeBacklogCode, refreshBacklogToken,
6
6
  import { logger } from '../utils/logger.js';
7
7
  const AUTH_CODE_TTL_MS = 10 * 60 * 1000;
8
8
  const REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
9
+ /** Seconds left until `expiresAt`, floored at zero so it is never negative. */
10
+ const remainingSeconds = (expiresAt) => Math.max(0, Math.floor((expiresAt - Date.now()) / 1000));
9
11
  const LOCALHOST_HOSTS = ['localhost', '127.0.0.1', '[::1]'];
10
12
  const SUPPORTED_AUTH_METHODS = ['client_secret_post', 'none'];
11
13
  function verifyPkce(codeVerifier, codeChallenge) {
@@ -278,6 +280,9 @@ export function createOAuthRoutes(config, store, mcpPath) {
278
280
  store.storeAuthCode(mcpCode, {
279
281
  mcpClientId: pending.mcpClientId,
280
282
  backlogTokens,
283
+ // Resolved here, against the response just received, rather than at
284
+ // `/token` where `expires_in` would be counted from the wrong instant.
285
+ backlogAccessTokenExpiresAt: Date.now() + backlogTokens.expires_in * 1000,
281
286
  codeChallenge: pending.codeChallenge,
282
287
  redirectUri: pending.redirectUri,
283
288
  resource: pending.resource,
@@ -336,7 +341,7 @@ export function createOAuthRoutes(config, store, mcpPath) {
336
341
  store.storeMcpToken(mcpAccessToken, {
337
342
  backlogAccessToken: entry.backlogTokens.access_token,
338
343
  clientId,
339
- expiresAt: Date.now() + entry.backlogTokens.expires_in * 1000,
344
+ expiresAt: entry.backlogAccessTokenExpiresAt,
340
345
  });
341
346
  store.storeMcpRefreshToken(mcpRefreshToken, {
342
347
  backlogRefreshToken: entry.backlogTokens.refresh_token,
@@ -346,7 +351,10 @@ export function createOAuthRoutes(config, store, mcpPath) {
346
351
  return c.json({
347
352
  access_token: mcpAccessToken,
348
353
  token_type: 'bearer',
349
- expires_in: entry.backlogTokens.expires_in,
354
+ // What is left, not what Backlog reported at `/callback`. A client
355
+ // uses this to decide when to refresh, so handing it the original
356
+ // duration would have it wait past the real expiry.
357
+ expires_in: remainingSeconds(entry.backlogAccessTokenExpiresAt),
350
358
  refresh_token: mcpRefreshToken,
351
359
  });
352
360
  }
@@ -28,6 +28,17 @@ type PendingAuthorization = {
28
28
  type AuthCodeEntry = {
29
29
  mcpClientId: string;
30
30
  backlogTokens: BacklogTokenData;
31
+ /**
32
+ * When the Backlog access token in `backlogTokens` actually expires, in
33
+ * epoch milliseconds, resolved at `/callback`.
34
+ *
35
+ * `backlogTokens.expires_in` is a duration counted from the moment Backlog
36
+ * answered, and this entry then sits here until the client redeems the code
37
+ * — up to `AUTH_CODE_TTL_MS`. Re-basing that duration at `/token` would put
38
+ * the expiry later than the real one by however long the redemption took,
39
+ * leaving a window where this server treats a spent Backlog token as live.
40
+ */
41
+ backlogAccessTokenExpiresAt: number;
31
42
  codeChallenge: string;
32
43
  redirectUri: string;
33
44
  resource?: string;
@@ -55,6 +66,16 @@ export declare function createTokenStore(): {
55
66
  cacheVerification(token: string, authInfo: AuthInfo, ttlMs: number): void;
56
67
  storeMcpToken(mcpToken: string, entry: McpTokenEntry): void;
57
68
  getMcpToken(mcpToken: string): McpTokenEntry | undefined;
69
+ /**
70
+ * Drops an MCP access token and its cached verification, so the next
71
+ * request on it is rejected by the bearer middleware.
72
+ *
73
+ * The refresh token is deliberately left in place: a Backlog 401 says the
74
+ * access token is spent, not that the grant is gone, so a client should be
75
+ * able to recover through the refresh grant before falling back to a full
76
+ * re-authorization.
77
+ */
78
+ revokeMcpToken(mcpToken: string): void;
58
79
  storeMcpRefreshToken(mcpRefreshToken: string, entry: McpRefreshEntry): void;
59
80
  consumeMcpRefreshToken(mcpRefreshToken: string): McpRefreshEntry | undefined;
60
81
  cleanup(): void;
@@ -85,6 +85,19 @@ export function createTokenStore() {
85
85
  }
86
86
  return entry;
87
87
  },
88
+ /**
89
+ * Drops an MCP access token and its cached verification, so the next
90
+ * request on it is rejected by the bearer middleware.
91
+ *
92
+ * The refresh token is deliberately left in place: a Backlog 401 says the
93
+ * access token is spent, not that the grant is gone, so a client should be
94
+ * able to recover through the refresh grant before falling back to a full
95
+ * re-authorization.
96
+ */
97
+ revokeMcpToken(mcpToken) {
98
+ mcpAccessTokens.delete(mcpToken);
99
+ verificationCache.delete(mcpToken);
100
+ },
88
101
  storeMcpRefreshToken(mcpRefreshToken, entry) {
89
102
  mcpRefreshTokens.set(mcpRefreshToken, entry);
90
103
  },
@@ -1,6 +1,20 @@
1
+ import { getCurrentAccessToken, reportBacklogAuthError, } from '../auth/backlogAuthContext.js';
1
2
  import { parseBacklogAPIError } from './parseBacklogAPIError.js';
2
3
  export const backlogErrorHandler = (err) => {
3
- const parsed = parseBacklogAPIError(err);
4
+ // Only an OAuth request carries an access token in its context; the stdio
5
+ // transport authenticates with an API key and never establishes one. The
6
+ // comparison is against `undefined` rather than truthiness so this asks
7
+ // exactly what `reportBacklogAuthError` asks — whether a context exists —
8
+ // and the two cannot answer differently for the same request.
9
+ const authMode = getCurrentAccessToken() !== undefined ? 'oauth' : 'apiKey';
10
+ const parsed = parseBacklogAPIError(err, { authMode });
11
+ // Backlog rejecting an OAuth token is authoritative: the credential this
12
+ // request was issued is spent. Reporting it lets the transport invalidate the
13
+ // token and tell the client to re-authenticate, instead of returning the
14
+ // failure as tool output the client cannot act on.
15
+ if (parsed.type === 'BacklogAuthError') {
16
+ reportBacklogAuthError();
17
+ }
4
18
  return {
5
19
  kind: 'error',
6
20
  message: parsed.message,
@@ -5,4 +5,11 @@ export type ParsedBacklogAPIError = {
5
5
  code?: number;
6
6
  url?: string;
7
7
  };
8
- export declare function parseBacklogAPIError(err: unknown): ParsedBacklogAPIError;
8
+ /**
9
+ * How the rejected request was authenticated. It decides only the wording of an
10
+ * authentication failure: in OAuth mode there is no API key to go and check.
11
+ */
12
+ export type BacklogAuthMode = 'apiKey' | 'oauth';
13
+ export declare function parseBacklogAPIError(err: unknown, options?: {
14
+ authMode?: BacklogAuthMode;
15
+ }): ParsedBacklogAPIError;
@@ -1,4 +1,8 @@
1
- export function parseBacklogAPIError(err) {
1
+ const authErrorMessage = (status, authMode) => authMode === 'oauth'
2
+ ? `Authentication failed (HTTP ${status}). The Backlog access token was rejected. Re-authenticate with Backlog, or check that your account has permission for this resource.`
3
+ : `Authentication failed (HTTP ${status}). Please check your API key or permissions.`;
4
+ export function parseBacklogAPIError(err, options = {}) {
5
+ const { authMode = 'apiKey' } = options;
2
6
  const e = err;
3
7
  if (e._name && e._status && e._url) {
4
8
  const status = e._status;
@@ -8,7 +12,7 @@ export function parseBacklogAPIError(err) {
8
12
  if (e._name === 'BacklogAuthError') {
9
13
  return {
10
14
  type: 'BacklogAuthError',
11
- message: `Authentication failed (HTTP ${status}). Please check your API key or permissions.`,
15
+ message: authErrorMessage(status, authMode),
12
16
  status,
13
17
  url,
14
18
  };
@@ -4,7 +4,6 @@ import { serve } from '@hono/node-server';
4
4
  import { hostHeaderValidation, localhostHostValidation, localhostOriginValidation, originValidation, } from '@modelcontextprotocol/hono';
5
5
  import { createMcpHandler } from '@modelcontextprotocol/server';
6
6
  import { Hono } from 'hono';
7
- import { runWithAccessToken } from './auth/backlogAuthContext.js';
8
7
  import { logger } from './utils/logger.js';
9
8
  const LOCALHOST_BINDS = ['127.0.0.1', 'localhost', '::1'];
10
9
  export const runHttpMcpServer = async (options) => {
@@ -58,14 +57,12 @@ export const runHttpMcpServer = async (options) => {
58
57
  responseMode: enableJsonResponse ? 'json' : 'auto',
59
58
  onerror: (err) => logger.error({ err }, 'MCP handler error'),
60
59
  });
60
+ // The bearer middleware owns the OAuth request context: it scopes the access
61
+ // token around this dispatch and converts a Backlog rejection into a 401.
61
62
  app.all(mcpPath, async (c) => {
62
63
  const authInfo = oauthEnabled ? c.get('authInfo') : undefined;
63
- const accessToken = authInfo?.token;
64
64
  try {
65
- const dispatch = () => mcpHandler.fetch(c.req.raw, { authInfo });
66
- return accessToken
67
- ? await runWithAccessToken(accessToken, dispatch)
68
- : await dispatch();
65
+ return await mcpHandler.fetch(c.req.raw, { authInfo });
69
66
  }
70
67
  catch (error) {
71
68
  logger.error({ err: error }, 'Error handling MCP request');
@@ -3,9 +3,10 @@ import type { Entity } from 'backlog-js';
3
3
  import { Backlog } from 'backlog-js';
4
4
  import { ToolDefinition } from '../types/tool.js';
5
5
  import { DescriptionHelper } from '../createDescriptionHelper.js';
6
- declare const deleteProjectSchema: (t: DescriptionHelper['t']) => {
6
+ declare const addCategorySchema: (t: DescriptionHelper['t']) => {
7
7
  projectId: z.ZodOptional<z.ZodNumber>;
8
8
  projectKey: z.ZodOptional<z.ZodString>;
9
+ name: z.ZodString;
9
10
  };
10
- export declare const deleteProjectTool: (backlog: Backlog, { t }: DescriptionHelper) => ToolDefinition<ReturnType<typeof deleteProjectSchema>, Entity.Project.Project>;
11
+ export declare const addCategoryTool: (backlog: Backlog, { t }: DescriptionHelper) => ToolDefinition<ReturnType<typeof addCategorySchema>, Entity.Project.Category>;
11
12
  export {};
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+ import { buildToolSchema } from '../types/tool.js';
3
+ import { outputFields } from '../types/outputFields.js';
4
+ import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
5
+ const addCategorySchema = buildToolSchema((t) => ({
6
+ projectId: z
7
+ .number()
8
+ .optional()
9
+ .describe(t('TOOL_ADD_CATEGORY_PROJECT_ID', 'The numeric ID of the project (e.g., 12345)')),
10
+ projectKey: z
11
+ .string()
12
+ .optional()
13
+ .describe(t('TOOL_ADD_CATEGORY_PROJECT_KEY', "The key of the project (e.g., 'PROJECT')")),
14
+ name: z
15
+ .string()
16
+ .describe(t('TOOL_ADD_CATEGORY_NAME', 'The name of the category')),
17
+ }));
18
+ export const addCategoryTool = (backlog, { t }) => {
19
+ return {
20
+ name: 'add_category',
21
+ description: t('TOOL_ADD_CATEGORY_DESCRIPTION', 'Creates a new category for a project'),
22
+ schema: z.object(addCategorySchema(t)),
23
+ importantFields: ['id', 'projectId', 'name'],
24
+ returnsList: false,
25
+ outputFields: outputFields()([
26
+ 'id',
27
+ 'projectId',
28
+ 'name',
29
+ 'displayOrder',
30
+ ]),
31
+ handler: async ({ projectId, projectKey, ...params }) => {
32
+ const result = resolveIdOrKey('project', { id: projectId, key: projectKey }, t);
33
+ if (!result.ok) {
34
+ throw result.error;
35
+ }
36
+ return backlog.postCategories(result.value, params);
37
+ },
38
+ };
39
+ };
@@ -7,7 +7,7 @@ import { addWikiTool } from './addWiki.js';
7
7
  import { updateWikiTool } from './updateWiki.js';
8
8
  import { countIssuesTool } from './countIssues.js';
9
9
  import { deleteIssueTool } from './deleteIssue.js';
10
- import { deleteProjectTool } from './deleteProject.js';
10
+ import { addCategoryTool } from './addCategory.js';
11
11
  import { getCategoriesTool } from './getCategories.js';
12
12
  import { getCustomFieldsTool } from './getCustomFields.js';
13
13
  import { getGitRepositoriesTool } from './getGitRepositories.js';
@@ -87,7 +87,6 @@ export const allTools = (backlog, helper) => {
87
87
  getProjectTool(backlog, helper),
88
88
  getProjectUsersTool(backlog, helper),
89
89
  updateProjectTool(backlog, helper),
90
- deleteProjectTool(backlog, helper),
91
90
  ],
92
91
  },
93
92
  {
@@ -110,6 +109,7 @@ export const allTools = (backlog, helper) => {
110
109
  removeRelatedIssueTool(backlog, helper),
111
110
  getPrioritiesTool(backlog, helper),
112
111
  getCategoriesTool(backlog, helper),
112
+ addCategoryTool(backlog, helper),
113
113
  getCustomFieldsTool(backlog, helper),
114
114
  getIssueTypesTool(backlog, helper),
115
115
  getResolutionsTool(backlog, helper),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backlog-mcp-server",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "backlog-mcp-server": "./build/index.js"
@@ -42,20 +42,20 @@
42
42
  "js-yaml": "^5.4.1",
43
43
  "pino": "^10.3.1",
44
44
  "yargs": "^18.1.0",
45
- "zod": "^4.4.3"
45
+ "zod": "^4.5.4"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/js-yaml": "^4.0.9",
49
- "@types/node": "^26.4.0",
49
+ "@types/node": "^26.4.1",
50
50
  "@types/yargs": "^17.0.35",
51
- "@vitest/coverage-v8": "^4.1.11",
52
- "oxfmt": "^0.65.0",
53
- "oxlint": "^1.80.0",
51
+ "@vitest/coverage-v8": "^5.0.0",
52
+ "oxfmt": "^0.66.0",
53
+ "oxlint": "^1.81.0",
54
54
  "oxlint-tsgolint": "^7.0.2001",
55
55
  "pino-pretty": "^13.1.3",
56
- "tsx": "^4.23.12",
56
+ "tsx": "^4.23.13",
57
57
  "typescript": "^7.0.2",
58
- "vitest": "^4.1.11"
58
+ "vitest": "^5.0.0"
59
59
  },
60
60
  "scripts": {
61
61
  "preinstall": "npx only-allow pnpm",
@@ -1,49 +0,0 @@
1
- import { z } from 'zod';
2
- import { buildToolSchema } from '../types/tool.js';
3
- import { outputFields } from '../types/outputFields.js';
4
- import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
5
- const deleteProjectSchema = buildToolSchema((t) => ({
6
- projectId: z
7
- .number()
8
- .optional()
9
- .describe(t('TOOL_DELETE_PROJECT_PROJECT_ID', 'The numeric ID of the project (e.g., 12345)')),
10
- projectKey: z
11
- .string()
12
- .optional()
13
- .describe(t('TOOL_DELETE_PROJECT_PROJECT_KEY', "The key of the project (e.g., 'PROJECT')")),
14
- }));
15
- export const deleteProjectTool = (backlog, { t }) => {
16
- return {
17
- name: 'delete_project',
18
- description: t('TOOL_DELETE_PROJECT_DESCRIPTION', 'Deletes a project'),
19
- schema: z.object(deleteProjectSchema(t)),
20
- returnsList: false,
21
- outputFields: outputFields()([
22
- 'id',
23
- 'projectKey',
24
- 'name',
25
- 'chartEnabled',
26
- 'useResolvedForChart',
27
- 'subtaskingEnabled',
28
- 'projectLeaderCanEditProjectLeader',
29
- 'useWiki',
30
- 'useFileSharing',
31
- 'useWikiTreeView',
32
- 'useOriginalImageSizeAtWiki',
33
- 'useSubversion',
34
- 'useGit',
35
- 'textFormattingRule',
36
- 'archived',
37
- 'displayOrder',
38
- 'useDevAttributes',
39
- 'grandchildIssueEnabled',
40
- ]),
41
- handler: async ({ projectId, projectKey }) => {
42
- const result = resolveIdOrKey('project', { id: projectId, key: projectKey }, t);
43
- if (!result.ok) {
44
- throw result.error;
45
- }
46
- return backlog.deleteProject(result.value);
47
- },
48
- };
49
- };