backlog-mcp-server 0.20.1 → 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
@@ -307,6 +307,7 @@ Tools for managing issues, their comments, and related items like priorities, ca
307
307
  - `remove_related_issue`: Removes the relation between an issue and a related issue.
308
308
  - `get_priorities`: Returns list of priorities.
309
309
  - `get_categories`: Returns list of categories for a project.
310
+ - `add_category`: Creates a new category for a project.
310
311
  - `get_custom_fields`: Returns list of custom fields for a project.
311
312
  - `get_issue_types`: Returns list of issue types for a project.
312
313
  - `get_resolutions`: Returns list of issue resolutions.
@@ -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;
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ import type { Entity } from 'backlog-js';
3
+ import { Backlog } from 'backlog-js';
4
+ import { ToolDefinition } from '../types/tool.js';
5
+ import { DescriptionHelper } from '../createDescriptionHelper.js';
6
+ declare const addCategorySchema: (t: DescriptionHelper['t']) => {
7
+ projectId: z.ZodOptional<z.ZodNumber>;
8
+ projectKey: z.ZodOptional<z.ZodString>;
9
+ name: z.ZodString;
10
+ };
11
+ export declare const addCategoryTool: (backlog: Backlog, { t }: DescriptionHelper) => ToolDefinition<ReturnType<typeof addCategorySchema>, Entity.Project.Category>;
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,6 +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 { addCategoryTool } from './addCategory.js';
10
11
  import { getCategoriesTool } from './getCategories.js';
11
12
  import { getCustomFieldsTool } from './getCustomFields.js';
12
13
  import { getGitRepositoriesTool } from './getGitRepositories.js';
@@ -108,6 +109,7 @@ export const allTools = (backlog, helper) => {
108
109
  removeRelatedIssueTool(backlog, helper),
109
110
  getPrioritiesTool(backlog, helper),
110
111
  getCategoriesTool(backlog, helper),
112
+ addCategoryTool(backlog, helper),
111
113
  getCustomFieldsTool(backlog, helper),
112
114
  getIssueTypesTool(backlog, helper),
113
115
  getResolutionsTool(backlog, helper),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backlog-mcp-server",
3
- "version": "0.20.1",
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",