mcp-jira-stdio 1.2.0 → 1.4.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/README.md CHANGED
@@ -6,11 +6,9 @@
6
6
  [![npm version](https://badge.fury.io/js/mcp-jira-stdio.svg)](https://www.npmjs.com/package/mcp-jira-stdio)
7
7
  [![MCP Server](https://img.shields.io/badge/MCP-Server-blue)](https://glama.ai/mcp/servers)
8
8
 
9
- [![Docker Pulls](https://img.shields.io/docker/pulls/freema/mcp-jira-stdio)](https://hub.docker.com/r/freema/mcp-jira-stdio)
10
9
  [![CI](https://github.com/freema/mcp-jira-stdio/actions/workflows/ci.yml/badge.svg)](https://github.com/freema/mcp-jira-stdio/actions/workflows/ci.yml)
11
10
  [![codecov](https://codecov.io/gh/freema/mcp-jira-stdio/branch/main/graph/badge.svg)](https://codecov.io/gh/freema/mcp-jira-stdio)
12
11
  [![GitHub issues](https://img.shields.io/github/issues/freema/mcp-jira-stdio)](https://github.com/freema/mcp-jira-stdio/issues)
13
- [![Docker Image Size](https://img.shields.io/docker/image-size/freema/mcp-jira-stdio/latest)](https://hub.docker.com/r/freema/mcp-jira-stdio)
14
12
 
15
13
  <a href="https://glama.ai/mcp/servers/@freema/mcp-jira-stdio">
16
14
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@freema/mcp-jira-stdio/badge" />
@@ -212,6 +210,11 @@ npm run inspector:dev
212
210
  task inspector:dev
213
211
  ```
214
212
 
213
+ Notes:
214
+
215
+ - Startup no longer blocks on Jira connectivity. If Jira env vars are missing, the server still starts and lists tools; tool calls will fail with a clear auth error until you set `JIRA_BASE_URL`, `JIRA_EMAIL`, and `JIRA_API_TOKEN`.
216
+ - Connection testing runs only in development/test (`NODE_ENV=development` or `test`). Failures are logged but do not terminate the server, so the inspector can still display tools.
217
+
215
218
  ### Testing
216
219
 
217
220
  ```bash
@@ -314,6 +317,15 @@ task inspector:dev
314
317
  task check
315
318
  ```
316
319
 
320
+ If the inspector shows an SSE error and the server exits immediately, ensure you are not forcing an early exit with invalid credentials. With the current behavior, the server should not exit on missing credentials; export your Jira vars to exercise the tools:
321
+
322
+ ```bash
323
+ export JIRA_BASE_URL="https://your-instance.atlassian.net"
324
+ export JIRA_EMAIL="your-email@example.com"
325
+ export JIRA_API_TOKEN="your-api-token"
326
+ npm run inspector
327
+ ```
328
+
317
329
  ## 🔍 Environment Variables
318
330
 
319
331
  | Variable | Required | Description | Example |
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ var require_package = __commonJS({
42
42
  "node_modules/dotenv/package.json"(exports, module) {
43
43
  module.exports = {
44
44
  name: "dotenv",
45
- version: "17.2.1",
45
+ version: "17.2.3",
46
46
  description: "Loads environment variables from .env file",
47
47
  main: "lib/main.js",
48
48
  types: "lib/main.d.ts",
@@ -64,8 +64,8 @@ var require_package = __commonJS({
64
64
  "dts-check": "tsc --project tests/types/tsconfig.json",
65
65
  lint: "standard",
66
66
  pretest: "npm run lint && npm run dts-check",
67
- test: "tap run --allow-empty-coverage --disable-coverage --timeout=60000",
68
- "test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
67
+ test: "tap run tests/**/*.js --allow-empty-coverage --disable-coverage --timeout=60000",
68
+ "test:coverage": "tap run tests/**/*.js --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
69
69
  prerelease: "npm test",
70
70
  release: "standard-version"
71
71
  },
@@ -118,9 +118,12 @@ var require_main = __commonJS({
118
118
  "\u{1F510} encrypt with Dotenvx: https://dotenvx.com",
119
119
  "\u{1F510} prevent committing .env to code: https://dotenvx.com/precommit",
120
120
  "\u{1F510} prevent building .env in docker: https://dotenvx.com/prebuild",
121
- "\u{1F4E1} observe env with Radar: https://dotenvx.com/radar",
122
- "\u{1F4E1} auto-backup env with Radar: https://dotenvx.com/radar",
123
- "\u{1F4E1} version env with Radar: https://dotenvx.com/radar",
121
+ "\u{1F4E1} add observability to secrets: https://dotenvx.com/ops",
122
+ "\u{1F465} sync secrets across teammates & machines: https://dotenvx.com/ops",
123
+ "\u{1F5C2}\uFE0F backup and recover secrets: https://dotenvx.com/ops",
124
+ "\u2705 audit secrets and track compliance: https://dotenvx.com/ops",
125
+ "\u{1F504} add secrets lifecycle management: https://dotenvx.com/ops",
126
+ "\u{1F511} add access controls to secrets: https://dotenvx.com/ops",
124
127
  "\u{1F6E0}\uFE0F run anywhere with `dotenvx run -- yourcommand`",
125
128
  "\u2699\uFE0F specify custom .env file path with { path: '/custom/path/.env' }",
126
129
  "\u2699\uFE0F enable debug logging with { debug: true }",
@@ -436,7 +439,7 @@ var require_package2 = __commonJS({
436
439
  "package.json"(exports, module) {
437
440
  module.exports = {
438
441
  name: "mcp-jira-stdio",
439
- version: "1.2.0",
442
+ version: "1.4.0",
440
443
  description: "Model Context Protocol (MCP) server for Jira API integration",
441
444
  author: "graslt",
442
445
  license: "MIT",
@@ -694,18 +697,6 @@ async function makeJiraRequest(config, retryConfig = {
694
697
  }
695
698
  throw lastError;
696
699
  }
697
- async function testConnection() {
698
- try {
699
- await makeJiraRequest({
700
- method: "GET",
701
- url: "/myself"
702
- });
703
- return true;
704
- } catch (error) {
705
- log.error("Connection test failed:", error);
706
- return false;
707
- }
708
- }
709
700
  function validateInput(schema, input) {
710
701
  try {
711
702
  return schema.parse(input);
@@ -727,13 +718,10 @@ function isValidProjectKey(key) {
727
718
  function sanitizeJQL(jql) {
728
719
  return jql.replace(/[<>"'`]/g, "").replace(/\s+/g, " ").trim();
729
720
  }
730
- function validatePagination(startAt, maxResults) {
731
- if (startAt < 0) {
732
- throw new Error("startAt must be non-negative");
733
- }
734
- if (maxResults < 1 || maxResults > 100) {
735
- throw new Error("maxResults must be between 1 and 100");
736
- }
721
+ function extractIssueKey(input) {
722
+ if (typeof input !== "string") return null;
723
+ const match = input.match(/[A-Z][A-Z0-9]*-\d+/);
724
+ return match ? match[0] : null;
737
725
  }
738
726
 
739
727
  // src/types/tools.ts
@@ -750,8 +738,10 @@ var GetIssueInputSchema = z.object({
750
738
  });
751
739
  var SearchIssuesInputSchema = z.object({
752
740
  jql: z.string().min(1).describe("JQL query string"),
753
- startAt: z.number().min(0).default(0).describe("Index of first result to return"),
754
- maxResults: z.number().min(1).max(100).default(50).describe("Maximum number of results to return"),
741
+ nextPageToken: z.string().optional().describe(
742
+ "Token for pagination. Omit for first page, use value from previous response for next page."
743
+ ),
744
+ maxResults: z.number().min(1).max(100).default(50).describe("Maximum number of results to return per page"),
755
745
  fields: z.array(z.string()).optional().describe("Specific fields to retrieve"),
756
746
  expand: z.array(z.string()).optional().describe("Additional details to include")
757
747
  });
@@ -780,8 +770,10 @@ var UpdateIssueInputSchema = z.object({
780
770
  returnIssue: z.boolean().optional().describe("When false, skip fetching full issue after update")
781
771
  });
782
772
  var GetMyIssuesInputSchema = z.object({
783
- startAt: z.number().min(0).default(0).describe("Index of first result to return"),
784
- maxResults: z.number().min(1).max(100).default(50).describe("Maximum number of results to return"),
773
+ nextPageToken: z.string().optional().describe(
774
+ "Token for pagination. Omit for first page, use value from previous response for next page."
775
+ ),
776
+ maxResults: z.number().min(1).max(100).default(50).describe("Maximum number of results to return per page"),
785
777
  fields: z.array(z.string()).optional().describe("Specific fields to retrieve"),
786
778
  expand: z.array(z.string()).optional().describe("Additional details to include")
787
779
  });
@@ -968,14 +960,15 @@ async function getIssue(issueKey, options = {}) {
968
960
  return await makeJiraRequest(config);
969
961
  }
970
962
  async function searchIssues(options) {
971
- const { jql, startAt = 0, maxResults = 50 } = options;
972
- validatePagination(startAt, maxResults);
963
+ const { jql, nextPageToken, maxResults = 50 } = options;
973
964
  const sanitizedJql = sanitizeJQL(jql);
974
965
  const data = {
975
966
  jql: sanitizedJql,
976
- startAt,
977
967
  maxResults
978
968
  };
969
+ if (nextPageToken) {
970
+ data.nextPageToken = nextPageToken;
971
+ }
979
972
  if (options.fields) {
980
973
  data.fields = options.fields;
981
974
  }
@@ -984,7 +977,7 @@ async function searchIssues(options) {
984
977
  }
985
978
  const config = {
986
979
  method: "POST",
987
- url: "/search",
980
+ url: "/search/jql",
988
981
  data
989
982
  };
990
983
  return await makeJiraRequest(config);
@@ -1059,9 +1052,9 @@ async function getMyIssues(options = {}) {
1059
1052
  const jql = `assignee = currentUser() ORDER BY updated DESC`;
1060
1053
  const searchParams = {
1061
1054
  jql,
1062
- startAt: options.startAt || 0,
1063
1055
  maxResults: options.maxResults || 50
1064
1056
  };
1057
+ if (options.nextPageToken !== void 0) searchParams.nextPageToken = options.nextPageToken;
1065
1058
  if (options.fields !== void 0) searchParams.fields = options.fields;
1066
1059
  if (options.expand !== void 0) searchParams.expand = options.expand;
1067
1060
  return await searchIssues(searchParams);
@@ -1321,9 +1314,18 @@ function formatSearchResultsResponse(result) {
1321
1314
  const rangeStart = result.startAt + 1;
1322
1315
  const rangeEnd = Math.min(result.startAt + result.maxResults, result.total);
1323
1316
  const showRangeInline = result.total > result.maxResults || result.startAt > 0;
1324
- const paginationInfo = showRangeInline ? `
1317
+ let paginationInfo = "";
1318
+ if (showRangeInline) {
1319
+ paginationInfo = `
1325
1320
 
1326
- *Showing ${rangeStart}-${rangeEnd} of ${result.total} results*` : "";
1321
+ *Showing ${rangeStart}-${rangeEnd} of ${result.total} results*`;
1322
+ }
1323
+ if (result.nextPageToken && !result.isLast) {
1324
+ paginationInfo += `
1325
+ *Next page token: \`${result.nextPageToken}\`*`;
1326
+ } else if (result.isLast) {
1327
+ paginationInfo += "\n*This is the last page of results*";
1328
+ }
1327
1329
  return {
1328
1330
  content: [
1329
1331
  {
@@ -1639,7 +1641,8 @@ var getIssueTool = {
1639
1641
  async function handleGetIssue(input) {
1640
1642
  try {
1641
1643
  const validated = validateInput(GetIssueInputSchema, input);
1642
- const key = (validated.issueKey || "").trim();
1644
+ const trimmedInput = (validated.issueKey || "").trim();
1645
+ const key = extractIssueKey(trimmedInput) || trimmedInput;
1643
1646
  log4.info(`Getting issue ${key}...`);
1644
1647
  const getParams = {};
1645
1648
  if (validated.expand !== void 0) getParams.expand = validated.expand;
@@ -1657,7 +1660,7 @@ async function handleGetIssue(input) {
1657
1660
  var log5 = createLogger("tool:search-issues");
1658
1661
  var searchIssuesTool = {
1659
1662
  name: TOOL_NAMES.SEARCH_ISSUES,
1660
- description: 'Search for Jira issues using JQL. Supports complex queries with pagination and field selection. Examples: "project = PROJECT AND status = Open", "assignee = currentUser()".',
1663
+ description: 'Search for Jira issues using JQL. Supports complex queries with pagination and field selection. Examples: "project = PROJECT AND status = Open", "assignee = currentUser()". For pagination, use nextPageToken from previous response.',
1661
1664
  inputSchema: {
1662
1665
  type: "object",
1663
1666
  properties: {
@@ -1665,15 +1668,13 @@ var searchIssuesTool = {
1665
1668
  type: "string",
1666
1669
  description: 'JQL query string (e.g., "project = PROJECT AND status = Open")'
1667
1670
  },
1668
- startAt: {
1669
- type: "number",
1670
- description: "Index of first result to return (for pagination)",
1671
- minimum: 0,
1672
- default: 0
1671
+ nextPageToken: {
1672
+ type: "string",
1673
+ description: "Token for pagination. Omit for first page, use value from previous response for next page."
1673
1674
  },
1674
1675
  maxResults: {
1675
1676
  type: "number",
1676
- description: "Maximum number of results to return",
1677
+ description: "Maximum number of results to return per page",
1677
1678
  minimum: 1,
1678
1679
  maximum: 100,
1679
1680
  default: 50
@@ -1700,7 +1701,7 @@ async function handleSearchIssues(input) {
1700
1701
  const searchParams = {
1701
1702
  jql: validated.jql
1702
1703
  };
1703
- if (validated.startAt !== void 0) searchParams.startAt = validated.startAt;
1704
+ if (validated.nextPageToken !== void 0) searchParams.nextPageToken = validated.nextPageToken;
1704
1705
  if (validated.maxResults !== void 0) searchParams.maxResults = validated.maxResults;
1705
1706
  if (validated.fields !== void 0) searchParams.fields = validated.fields;
1706
1707
  if (validated.expand !== void 0) searchParams.expand = validated.expand;
@@ -1717,19 +1718,17 @@ async function handleSearchIssues(input) {
1717
1718
  var log6 = createLogger("tool:get-my-issues");
1718
1719
  var getMyIssuesTool = {
1719
1720
  name: TOOL_NAMES.GET_MY_ISSUES,
1720
- description: "Retrieves issues assigned to current user, sorted by most recently updated first. Supports pagination and field selection.",
1721
+ description: "Retrieves issues assigned to current user, sorted by most recently updated first. Supports pagination and field selection. For pagination, use nextPageToken from previous response.",
1721
1722
  inputSchema: {
1722
1723
  type: "object",
1723
1724
  properties: {
1724
- startAt: {
1725
- type: "number",
1726
- description: "Index of first result to return (for pagination)",
1727
- minimum: 0,
1728
- default: 0
1725
+ nextPageToken: {
1726
+ type: "string",
1727
+ description: "Token for pagination. Omit for first page, use value from previous response for next page."
1729
1728
  },
1730
1729
  maxResults: {
1731
1730
  type: "number",
1732
- description: "Maximum number of results to return",
1731
+ description: "Maximum number of results to return per page",
1733
1732
  minimum: 1,
1734
1733
  maximum: 100,
1735
1734
  default: 50
@@ -1754,7 +1753,7 @@ async function handleGetMyIssues(input) {
1754
1753
  const validated = validateInput(GetMyIssuesInputSchema, input);
1755
1754
  log6.info("Getting issues assigned to current user...");
1756
1755
  const getParams = {};
1757
- if (validated.startAt !== void 0) getParams.startAt = validated.startAt;
1756
+ if (validated.nextPageToken !== void 0) getParams.nextPageToken = validated.nextPageToken;
1758
1757
  if (validated.maxResults !== void 0) getParams.maxResults = validated.maxResults;
1759
1758
  if (validated.fields !== void 0) getParams.fields = validated.fields;
1760
1759
  if (validated.expand !== void 0) getParams.expand = validated.expand;
@@ -2291,25 +2290,22 @@ var allTools = [
2291
2290
  async function main() {
2292
2291
  const isDryRun = (process.env.DRY_RUN || "").toLowerCase() === "1" || (process.env.DRY_RUN || "").toLowerCase() === "true";
2293
2292
  if (!isDryRun) {
2294
- try {
2295
- const auth = validateAuth();
2296
- console.error(`\u{1F510} Authenticated as: ${auth.email}`);
2297
- console.error(`\u{1F310} Jira instance: ${auth.baseUrl}`);
2298
- console.error("\u{1F50D} Testing connection to Jira...");
2299
- const ok = await testConnection();
2300
- if (!ok) {
2301
- console.error("\u274C Connection to Jira failed");
2302
- process.exit(1);
2303
- return;
2293
+ const hasAuthVars = Boolean(
2294
+ process.env.JIRA_BASE_URL && process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN
2295
+ );
2296
+ if (hasAuthVars) {
2297
+ try {
2298
+ const auth = validateAuth();
2299
+ console.error(`\u{1F510} Authenticated as: ${auth.email}`);
2300
+ console.error(`\u{1F310} Jira instance: ${auth.baseUrl}`);
2301
+ } catch (error) {
2302
+ console.error("\u26A0\uFE0F Invalid Jira credentials:", error.message);
2304
2303
  }
2305
- console.error("\u2705 Connection to Jira successful");
2306
- } catch (error) {
2307
- console.error("\u274C Authentication Error:", error.message);
2308
- process.exit(1);
2309
- return;
2304
+ } else {
2305
+ console.error("\u26A0\uFE0F Jira env vars missing (JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN)");
2310
2306
  }
2311
2307
  } else {
2312
- console.error("\u{1F9EA} DRY_RUN=1 set \u2014 skipping Jira auth and connection test");
2308
+ console.error("\u{1F9EA} DRY_RUN=1 set \u2014 skipping Jira auth check");
2313
2309
  }
2314
2310
  let version = "0.0.0";
2315
2311
  try {