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 +1 -0
- package/build/auth/oauthRoutes.js +10 -2
- package/build/auth/tokenStore.d.ts +11 -0
- package/build/tools/addCategory.d.ts +12 -0
- package/build/tools/addCategory.js +39 -0
- package/build/tools/tools.js +2 -0
- package/package.json +8 -8
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:
|
|
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
|
-
|
|
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
|
+
};
|
package/build/tools/tools.js
CHANGED
|
@@ -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.
|
|
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
|
|
45
|
+
"zod": "^4.5.4"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/js-yaml": "^4.0.9",
|
|
49
|
-
"@types/node": "^26.4.
|
|
49
|
+
"@types/node": "^26.4.1",
|
|
50
50
|
"@types/yargs": "^17.0.35",
|
|
51
|
-
"@vitest/coverage-v8": "^
|
|
52
|
-
"oxfmt": "^0.
|
|
53
|
-
"oxlint": "^1.
|
|
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.
|
|
56
|
+
"tsx": "^4.23.13",
|
|
57
57
|
"typescript": "^7.0.2",
|
|
58
|
-
"vitest": "^
|
|
58
|
+
"vitest": "^5.0.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"preinstall": "npx only-allow pnpm",
|