@atlassian-dc-mcp/jira 0.17.0 → 0.18.0

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/src/config.ts CHANGED
@@ -12,7 +12,8 @@ export const JIRA_PRODUCT: ProductDefinition = {
12
12
  token: 'JIRA_API_TOKEN',
13
13
  defaultPageSize: 'JIRA_DEFAULT_PAGE_SIZE',
14
14
  },
15
- defaultApiBasePath: '/rest/api/2',
15
+ defaultApiBasePath: '/rest',
16
+ apiBasePathStrippableSuffixes: ['/api/2'],
16
17
  };
17
18
 
18
19
  export function getJiraRuntimeConfig() {
@@ -9,6 +9,9 @@ import { CancelablePromise } from './CancelablePromise.js';
9
9
  import type { OnCancel } from './CancelablePromise.js';
10
10
  import type { OpenAPIConfig } from './OpenAPI.js';
11
11
 
12
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
13
+ const REQUEST_TIMEOUT_MS_ENV_VAR = 'ATLASSIAN_DC_MCP_REQUEST_TIMEOUT_MS';
14
+
12
15
  export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
13
16
  return value !== undefined && value !== null;
14
17
  };
@@ -129,6 +132,15 @@ export const getFormData = (options: ApiRequestOptions): FormData | undefined =>
129
132
 
130
133
  type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
131
134
 
135
+ const getRequestTimeoutMs = (): number => {
136
+ const raw = process.env[REQUEST_TIMEOUT_MS_ENV_VAR];
137
+ if (!raw) {
138
+ return DEFAULT_REQUEST_TIMEOUT_MS;
139
+ }
140
+ const parsed = Number.parseInt(raw.trim(), 10);
141
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REQUEST_TIMEOUT_MS;
142
+ };
143
+
132
144
  export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
133
145
  if (typeof resolver === 'function') {
134
146
  return (resolver as Resolver<T>)(options);
@@ -202,6 +214,7 @@ export const sendRequest = async (
202
214
  onCancel: OnCancel
203
215
  ): Promise<Response> => {
204
216
  const controller = new AbortController();
217
+ const timeout = setTimeout(() => controller.abort(), getRequestTimeoutMs());
205
218
 
206
219
  const request: RequestInit = {
207
220
  headers,
@@ -214,9 +227,16 @@ export const sendRequest = async (
214
227
  request.credentials = config.CREDENTIALS;
215
228
  }
216
229
 
217
- onCancel(() => controller.abort());
230
+ onCancel(() => {
231
+ clearTimeout(timeout);
232
+ controller.abort();
233
+ });
218
234
 
219
- return await fetch(url, request);
235
+ try {
236
+ return await fetch(url, request);
237
+ } finally {
238
+ clearTimeout(timeout);
239
+ }
220
240
  };
221
241
 
222
242
  export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => {
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
- import { handleApiOperation } from '@atlassian-dc-mcp/common';
3
- import { IssueService, OpenAPI, SearchService } from './jira-client/index.js';
2
+ import { handleApiOperation, resolveOpenApiBase } from '@atlassian-dc-mcp/common';
3
+ import { IssueService, MyselfService, OpenAPI, SearchService } from './jira-client/index.js';
4
4
  import type { StringList } from './jira-client/models/StringList.js';
5
- import { getDefaultPageSize, getMissingConfig } from './config.js';
5
+ import { getDefaultPageSize, getMissingConfig, JIRA_PRODUCT } from './config.js';
6
6
 
7
7
  const DEFAULT_SEARCH_FIELDS = ['summary', 'description', 'status', 'assignee', 'reporter', 'priority', 'issuetype', 'labels', 'updated'];
8
8
  const DEFAULT_ISSUE_FIELDS = [...DEFAULT_SEARCH_FIELDS, 'parent', 'subtasks'];
@@ -28,17 +28,15 @@ export class JiraService {
28
28
  constructor(
29
29
  host: string | undefined,
30
30
  token: string | (() => string | undefined),
31
- fullBaseUrl?: string,
31
+ apiBasePath?: string,
32
32
  getPageSize: () => number = getDefaultPageSize,
33
33
  ) {
34
- if (fullBaseUrl) {
35
- OpenAPI.BASE = fullBaseUrl;
36
- } else if (host) {
37
- OpenAPI.BASE = `https://${host}/rest`;
38
- } else {
39
- throw new Error('Either host or fullBaseUrl must be provided');
40
- }
41
-
34
+ OpenAPI.BASE = resolveOpenApiBase({
35
+ host,
36
+ apiBasePath,
37
+ defaultBasePath: JIRA_PRODUCT.defaultApiBasePath ?? '/rest',
38
+ strippableSuffixes: JIRA_PRODUCT.apiBasePathStrippableSuffixes,
39
+ });
42
40
  OpenAPI.TOKEN = resolveToken(token, 'Missing required environment variable: JIRA_API_TOKEN');
43
41
  OpenAPI.VERSION = '2';
44
42
  this.getPageSize = getPageSize;
@@ -147,6 +145,10 @@ export class JiraService {
147
145
  }, 'Error transitioning issue');
148
146
  }
149
147
 
148
+ async validateSetup(): Promise<void> {
149
+ await MyselfService.getUser();
150
+ }
151
+
150
152
  static validateConfig(): string[] {
151
153
  return getMissingConfig();
152
154
  }
package/src/setup.ts CHANGED
@@ -1,4 +1,15 @@
1
- import { runSetup } from '@atlassian-dc-mcp/common';
1
+ import { describeValidationError, runSetup } from '@atlassian-dc-mcp/common';
2
2
  import { JIRA_PRODUCT } from './config.js';
3
+ import { JiraService } from './jira-service.js';
3
4
 
4
- await runSetup(JIRA_PRODUCT);
5
+ await runSetup(JIRA_PRODUCT, {
6
+ validateCredentials: async ({ host, apiBasePath, token }) => {
7
+ const service = new JiraService(host || undefined, token, apiBasePath || undefined);
8
+ try {
9
+ await service.validateSetup();
10
+ return { ok: true };
11
+ } catch (error) {
12
+ return { ok: false, message: describeValidationError(error) };
13
+ }
14
+ },
15
+ });