org-jira-mcp-adapter 1.2.2 → 1.2.3

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
@@ -22,14 +22,15 @@
22
22
  ],
23
23
  "env": {
24
24
  "JIRA_HOST": "jira.example.com",
25
- "JIRA_API_TOKEN": "your_pat_token"
25
+ "JIRA_API_TOKEN": "your_pat_token",
26
+ "CONFLUENCE_HOST": "https://confluence.example.com",
27
+ "CONFLUENCE_TOKEN": "your_confluence_pat"
26
28
  }
27
29
  }
28
30
  }
29
31
  }
30
- ```
31
32
 
32
- > **Важно**: Замените `jira.example.com` и `your_pat_token` на ваши реальные данные.
33
+ > **Важно**: Замените `jira.example.com`, `your_pat_token` и настройки Confluence на ваши реальные данные.
33
34
 
34
35
  ### Переменные окружения
35
36
 
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod';
2
+
3
+ export class ConfluenceService {
4
+ constructor(host, token) {
5
+ this.baseUrl = host.replace(/\/$/, '') + '/rest/api';
6
+ this.token = token;
7
+ this.headers = {
8
+ 'Authorization': `Bearer ${token}`,
9
+ 'Accept': 'application/json',
10
+ 'Content-Type': 'application/json'
11
+ };
12
+ }
13
+
14
+ async request(endpoint, options = {}) {
15
+ const url = `${this.baseUrl}${endpoint}`;
16
+ const response = await fetch(url, {
17
+ ...options,
18
+ headers: {
19
+ ...this.headers,
20
+ ...options.headers
21
+ }
22
+ });
23
+
24
+ if (!response.ok) {
25
+ const body = await response.text();
26
+ throw new Error(`Confluence API error: ${response.status} ${response.statusText} - ${body}`);
27
+ }
28
+
29
+ return response.json();
30
+ }
31
+
32
+ async searchPages(cql, limit = 10) {
33
+ const params = new URLSearchParams({
34
+ cql,
35
+ limit: limit.toString(),
36
+ expand: 'space,version,body.view'
37
+ });
38
+ return this.request(`/content/search?${params}`);
39
+ }
40
+
41
+ async getPage(pageId, expand) {
42
+ const expandParam = expand ? `&expand=${expand}` : '&expand=body.storage,version,space';
43
+ return this.request(`/content/${pageId}?${expandParam}`);
44
+ }
45
+
46
+ async getSpace(spaceKey) {
47
+ return this.request(`/space/${spaceKey}`);
48
+ }
49
+ }
50
+
51
+ export const confluenceToolSchemas = {
52
+ searchPages: {
53
+ cql: z.string().describe("Confluence Query Language (CQL) string"),
54
+ limit: z.number().optional().describe("Maximum number of results to return")
55
+ },
56
+ getPage: {
57
+ pageId: z.string().describe("Confluence Page ID"),
58
+ expand: z.string().optional().describe("Comma separated fields to expand")
59
+ },
60
+ getSpace: {
61
+ spaceKey: z.string().describe("Confluence Space Key")
62
+ }
63
+ };
package/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { connectServer, createMcpServer, formatToolResponse } from '@atlassian-dc-mcp/common';
3
3
  import { JiraService, jiraToolSchemas } from './jira-service.mjs';
4
+ import { ConfluenceService, confluenceToolSchemas } from './confluence-service.mjs';
4
5
  import * as process from 'node:process';
5
6
 
6
7
  const missingEnvVars = JiraService.validateConfig();
@@ -12,12 +13,45 @@ if (missingEnvVars.length > 0) {
12
13
  // Removing email from constructor as it is not needed for PAT auth
13
14
  const jiraService = new JiraService(process.env.JIRA_HOST, process.env.JIRA_API_TOKEN, process.env.JIRA_API_BASE_PATH);
14
15
 
16
+ // Confluence setup - используем CONFLUENCE_TOKEN если есть, иначе пробуем JIRA_API_TOKEN
17
+ const confluenceToken = process.env.CONFLUENCE_TOKEN || process.env.JIRA_API_TOKEN;
18
+ const confluenceHost = process.env.CONFLUENCE_HOST;
19
+
20
+ if (!confluenceHost) {
21
+ console.error("Warning: CONFLUENCE_HOST is not set. Confluence tools will fail.");
22
+ }
23
+
24
+ const confluenceService = new ConfluenceService(
25
+ confluenceHost || '',
26
+ confluenceToken
27
+ );
28
+
15
29
  const server = createMcpServer({
16
30
  name: "atlassian-jira-mcp",
17
31
  version: "0.1.0"
18
32
  });
19
33
 
20
34
  const jiraInstanceType = "JIRA Data Center edition instance";
35
+ const confluenceInstanceType = "Confluence Data Center instance";
36
+
37
+ // --- Confluence Tools ---
38
+
39
+ server.tool("confluence_searchPages", `Search for Confluence pages using CQL in the ${confluenceInstanceType}`, confluenceToolSchemas.searchPages, async ({ cql, limit }) => {
40
+ const result = await confluenceService.searchPages(cql, limit);
41
+ return formatToolResponse(result);
42
+ });
43
+
44
+ server.tool("confluence_getPage", `Get details of a Confluence page by its ID from the ${confluenceInstanceType}`, confluenceToolSchemas.getPage, async ({ pageId, expand }) => {
45
+ const result = await confluenceService.getPage(pageId, expand);
46
+ return formatToolResponse(result);
47
+ });
48
+
49
+ server.tool("confluence_getSpace", `Get details of a Confluence space by its key from the ${confluenceInstanceType}`, confluenceToolSchemas.getSpace, async ({ spaceKey }) => {
50
+ const result = await confluenceService.getSpace(spaceKey);
51
+ return formatToolResponse(result);
52
+ });
53
+
54
+ // --- Jira Tools ---
21
55
 
22
56
  server.tool("jira_searchIssues", `Search for JIRA issues using JQL in the ${jiraInstanceType}`, jiraToolSchemas.searchIssues, async ({ jql, expand, startAt, maxResults = 10 }) => {
23
57
  const result = await jiraService.searchIssues(jql, startAt, expand, maxResults);
@@ -124,4 +158,19 @@ server.tool("jira_findUsers", `Find users in the ${jiraInstanceType}`, jiraToolS
124
158
  return formatToolResponse(result);
125
159
  });
126
160
 
161
+ server.tool("jira_getIssueAttachments", `Get attachments for a JIRA issue in the ${jiraInstanceType}`, jiraToolSchemas.getIssueAttachments, async ({ issueKey }) => {
162
+ const result = await jiraService.getIssueAttachments(issueKey);
163
+ return formatToolResponse(result);
164
+ });
165
+
166
+ server.tool("jira_getAttachmentContentUrl", `Get download URL for a JIRA attachment in the ${jiraInstanceType}`, jiraToolSchemas.getAttachmentContentUrl, async ({ attachmentId }) => {
167
+ const url = await jiraService.getAttachmentContentUrl(attachmentId);
168
+ return formatToolResponse({ url, attachmentId });
169
+ });
170
+
171
+ server.tool("jira_addAttachment", `Add an attachment to a JIRA issue in the ${jiraInstanceType}`, jiraToolSchemas.addAttachment, async ({ issueKey, filePath }) => {
172
+ const result = await jiraService.addAttachment(issueKey, filePath);
173
+ return formatToolResponse(result);
174
+ });
175
+
127
176
  await connectServer(server);
package/jira-service.mjs CHANGED
@@ -1,6 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { handleApiOperation } from '@atlassian-dc-mcp/common';
3
- import { IssueService, OpenAPI, SearchService } from '@atlassian-dc-mcp/jira/build/jira-client/index.js';
3
+ import { IssueService, OpenAPI, SearchService, ProjectService, MyselfService, GroupuserpickerService } from '@atlassian-dc-mcp/jira/build/jira-client/index.js';
4
+ import { request } from '@atlassian-dc-mcp/jira/build/jira-client/core/request.js';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
4
7
 
5
8
  export class JiraService {
6
9
  constructor(host, token, fullBaseUrl) {
@@ -130,6 +133,91 @@ export class JiraService {
130
133
  return handleApiOperation(() => GroupuserpickerService.findUsersAndGroups(undefined, maxResults, query, undefined, undefined, undefined), 'Error finding users');
131
134
  }
132
135
 
136
+ async getIssueAttachments(issueKey) {
137
+ return handleApiOperation(async () => {
138
+ const issue = await IssueService.getIssue(issueKey, ['attachment']);
139
+ const attachments = issue.fields?.attachment || [];
140
+ // Attachment'ы уже содержат поле 'content' с URL для скачивания
141
+ return attachments;
142
+ }, 'Error getting issue attachments');
143
+ }
144
+
145
+ async getAttachmentContentUrl(attachmentId) {
146
+ // Возвращает URL для скачивания attachment'а
147
+ // В Jira REST API attachment'ы доступны через /secure/attachment/{id}
148
+ const baseUrl = OpenAPI.BASE.replace('/rest', '');
149
+ return `${baseUrl}/secure/attachment/${attachmentId}`;
150
+ }
151
+
152
+ async addAttachment(issueKey, filePath) {
153
+ return handleApiOperation(async () => {
154
+ // Проверяем существование файла
155
+ if (!fs.existsSync(filePath)) {
156
+ throw new Error(`File not found: ${filePath}`);
157
+ }
158
+
159
+ // Читаем файл
160
+ const fileBuffer = fs.readFileSync(filePath);
161
+ const fileName = path.basename(filePath);
162
+
163
+ // Создаем File из буфера (в Node.js 18+ доступен глобальный File)
164
+ // File наследуется от Blob и содержит имя файла
165
+ let file;
166
+ if (typeof File !== 'undefined') {
167
+ file = new File([fileBuffer], fileName, {
168
+ type: this.getMimeType(filePath)
169
+ });
170
+ } else {
171
+ // Если File недоступен, создаем Blob и добавляем имя через специальный объект
172
+ const blob = new Blob([fileBuffer], {
173
+ type: this.getMimeType(filePath)
174
+ });
175
+ // Создаем объект, который будет правильно обработан FormData
176
+ file = Object.assign(blob, { name: fileName });
177
+ }
178
+
179
+ // Для Jira API нужно передать объект с ключом 'file' и значением File/Blob
180
+ // Используем request напрямую для правильной обработки multipart/form-data
181
+ // Также нужно добавить заголовок X-Atlassian-Token: no-check для защиты от XSRF
182
+ return request(OpenAPI, {
183
+ method: 'POST',
184
+ url: '/api/2/issue/{issueIdOrKey}/attachments',
185
+ path: {
186
+ 'issueIdOrKey': issueKey,
187
+ },
188
+ headers: {
189
+ 'X-Atlassian-Token': 'no-check'
190
+ },
191
+ formData: {
192
+ 'file': file
193
+ },
194
+ mediaType: 'multipart/form-data',
195
+ errors: {
196
+ 403: `Returned if attachments is disabled or if you don't have permission to add attachments to this issue.`,
197
+ 404: `Returned if the requested issue is not found, the user does not have permission to view it, or if the attachments exceeds the maximum configured attachment size.`,
198
+ },
199
+ });
200
+ }, 'Error adding attachment');
201
+ }
202
+
203
+ getMimeType(filePath) {
204
+ const ext = path.extname(filePath).toLowerCase();
205
+ const mimeTypes = {
206
+ '.png': 'image/png',
207
+ '.jpg': 'image/jpeg',
208
+ '.jpeg': 'image/jpeg',
209
+ '.gif': 'image/gif',
210
+ '.pdf': 'application/pdf',
211
+ '.txt': 'text/plain',
212
+ '.doc': 'application/msword',
213
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
214
+ '.xls': 'application/vnd.ms-excel',
215
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
216
+ '.zip': 'application/zip',
217
+ };
218
+ return mimeTypes[ext] || 'application/octet-stream';
219
+ }
220
+
133
221
  static validateConfig() {
134
222
  const requiredEnvVars = ['JIRA_API_TOKEN'];
135
223
  const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);
@@ -227,5 +315,15 @@ export const jiraToolSchemas = {
227
315
  description: z.string().optional().describe("New description in JIRA Wiki Markup (optional)"),
228
316
  issueTypeId: z.string().optional().describe("New issue type id (optional)"),
229
317
  customFields: z.object({}).catchall(z.any()).optional().describe("Optional custom fields to update as key-value pairs. Examples: {'customfield_10001': 'Custom Value', 'priority': {'id': '1'}, 'assignee': {'name': 'john.doe'}, 'labels': ['urgent', 'bug']}")
318
+ },
319
+ getIssueAttachments: {
320
+ issueKey: z.string().describe("JIRA issue key (e.g., PROJ-123)")
321
+ },
322
+ getAttachmentContentUrl: {
323
+ attachmentId: z.string().describe("Attachment ID from JIRA issue")
324
+ },
325
+ addAttachment: {
326
+ issueKey: z.string().describe("JIRA issue key (e.g., PROJ-123)"),
327
+ filePath: z.string().describe("Path to the file to attach (absolute or relative path)")
230
328
  }
231
329
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "org-jira-mcp-adapter",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "MCP server for Jira Data Center/Server (On-Premise)",
5
5
  "type": "module",
6
6
  "main": "index.mjs",