@rui.branco/jira-mcp 1.7.5 → 1.7.7
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 +128 -10
- package/index.js +692 -112
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -53,6 +53,15 @@ const jiraConfigPath = path.join(
|
|
|
53
53
|
);
|
|
54
54
|
const rawConfig = JSON.parse(fs.readFileSync(jiraConfigPath, "utf8"));
|
|
55
55
|
|
|
56
|
+
// Confluence tool visibility: 'on-demand' (default) hides the confluence_*
|
|
57
|
+
// tools until confluence_enable is called; 'always' exposes them from boot.
|
|
58
|
+
// Env JIRA_MCP_CONFLUENCE overrides the config key.
|
|
59
|
+
const confluenceMode = String(process.env.JIRA_MCP_CONFLUENCE || rawConfig.confluence || "on-demand").toLowerCase();
|
|
60
|
+
if (confluenceMode !== "always" && confluenceMode !== "on-demand") {
|
|
61
|
+
console.error(`jira-mcp: invalid confluence mode "${confluenceMode}", falling back to "on-demand"`);
|
|
62
|
+
}
|
|
63
|
+
let confluenceActive = confluenceMode === "always";
|
|
64
|
+
|
|
56
65
|
// Normalize config to multi-instance format
|
|
57
66
|
let instances;
|
|
58
67
|
if (rawConfig.instances) {
|
|
@@ -134,10 +143,323 @@ if (!fs.existsSync(attachmentDir)) {
|
|
|
134
143
|
fs.mkdirSync(attachmentDir, { recursive: true });
|
|
135
144
|
}
|
|
136
145
|
|
|
146
|
+
// ============ TOOL METADATA + POLICY ============
|
|
147
|
+
//
|
|
148
|
+
// TOOL_METADATA is the source of truth for op classification — NOT the HTTP
|
|
149
|
+
// method. The new Jira /search/jql endpoint is POST but semantically a read,
|
|
150
|
+
// so deciding "is this a write?" from the request method would wrongly trip
|
|
151
|
+
// audit + rate-limit on every search.
|
|
152
|
+
|
|
153
|
+
const TOOL_METADATA = {
|
|
154
|
+
// ---- Jira reads ----
|
|
155
|
+
jira_get_myself: { product: "jira", op: "read" },
|
|
156
|
+
jira_get_ticket: { product: "jira", op: "read" },
|
|
157
|
+
jira_search: { product: "jira", op: "read" },
|
|
158
|
+
jira_search_users: { product: "jira", op: "read" },
|
|
159
|
+
jira_get_changelog: { product: "jira", op: "read" },
|
|
160
|
+
jira_list_instances: { product: "jira", op: "read" },
|
|
161
|
+
jira_get_teams: { product: "jira", op: "read" },
|
|
162
|
+
jira_get_link_types: { product: "jira", op: "read" },
|
|
163
|
+
jira_get_transitions: { product: "jira", op: "read" },
|
|
164
|
+
jira_get_worklogs: { product: "jira", op: "read" },
|
|
165
|
+
jira_get_sprints: { product: "jira", op: "read" },
|
|
166
|
+
jira_get_boards: { product: "jira", op: "read" },
|
|
167
|
+
jira_get_issue_types: { product: "jira", op: "read" },
|
|
168
|
+
jira_get_priorities: { product: "jira", op: "read" },
|
|
169
|
+
jira_get_components: { product: "jira", op: "read" },
|
|
170
|
+
jira_get_versions: { product: "jira", op: "read" },
|
|
171
|
+
// ---- Jira writes (reversible) ----
|
|
172
|
+
jira_add_comment: { product: "jira", op: "write", category: "comment" },
|
|
173
|
+
jira_reply_comment: { product: "jira", op: "write", category: "comment" },
|
|
174
|
+
jira_edit_comment: { product: "jira", op: "write", category: "comment" },
|
|
175
|
+
jira_transition: { product: "jira", op: "write", category: "transition" },
|
|
176
|
+
jira_update_ticket: { product: "jira", op: "write", category: "ticket" },
|
|
177
|
+
jira_create_ticket: { product: "jira", op: "write", category: "ticket" },
|
|
178
|
+
jira_create_subtask: { product: "jira", op: "write", category: "ticket" },
|
|
179
|
+
jira_clone_ticket: { product: "jira", op: "write", category: "ticket" },
|
|
180
|
+
jira_link_tickets: { product: "jira", op: "write", category: "link" },
|
|
181
|
+
jira_unlink_tickets: { product: "jira", op: "write", category: "link" },
|
|
182
|
+
jira_add_attachment: { product: "jira", op: "write", category: "attachment" },
|
|
183
|
+
jira_add_watcher: { product: "jira", op: "write", category: "watcher" },
|
|
184
|
+
jira_remove_watcher: { product: "jira", op: "write", category: "watcher" },
|
|
185
|
+
jira_add_worklog: { product: "jira", op: "write", category: "worklog" },
|
|
186
|
+
jira_move_to_sprint: { product: "jira", op: "write", category: "sprint" },
|
|
187
|
+
jira_add_instance: { product: "jira", op: "write", category: "config" },
|
|
188
|
+
// ---- Jira destructive ----
|
|
189
|
+
jira_delete_comment: { product: "jira", op: "destructive", category: "comment" },
|
|
190
|
+
jira_delete_ticket: { product: "jira", op: "destructive", category: "ticket" },
|
|
191
|
+
jira_remove_attachment: { product: "jira", op: "destructive", category: "attachment" },
|
|
192
|
+
jira_remove_instance: { product: "jira", op: "destructive", category: "config" },
|
|
193
|
+
// ---- Confluence activation ----
|
|
194
|
+
confluence_enable: { product: "confluence", op: "read" },
|
|
195
|
+
// ---- Confluence reads ----
|
|
196
|
+
confluence_get_spaces: { product: "confluence", op: "read" },
|
|
197
|
+
confluence_get_space: { product: "confluence", op: "read" },
|
|
198
|
+
confluence_search: { product: "confluence", op: "read" },
|
|
199
|
+
confluence_get_recent_pages: { product: "confluence", op: "read" },
|
|
200
|
+
confluence_get_space_root_pages: { product: "confluence", op: "read" },
|
|
201
|
+
confluence_get_page_children: { product: "confluence", op: "read" },
|
|
202
|
+
confluence_get_page: { product: "confluence", op: "read" },
|
|
203
|
+
confluence_get_comments: { product: "confluence", op: "read" },
|
|
204
|
+
confluence_get_labels: { product: "confluence", op: "read" },
|
|
205
|
+
confluence_list_attachments: { product: "confluence", op: "read" },
|
|
206
|
+
confluence_download_attachment: { product: "confluence", op: "read" },
|
|
207
|
+
// ---- Confluence writes ----
|
|
208
|
+
confluence_create_space: { product: "confluence", op: "write", category: "space" },
|
|
209
|
+
confluence_update_space: { product: "confluence", op: "write", category: "space" },
|
|
210
|
+
confluence_add_comment: { product: "confluence", op: "write", category: "comment" },
|
|
211
|
+
confluence_update_page: { product: "confluence", op: "write", category: "page" },
|
|
212
|
+
confluence_create_page: { product: "confluence", op: "write", category: "page" },
|
|
213
|
+
confluence_add_label: { product: "confluence", op: "write", category: "label" },
|
|
214
|
+
confluence_remove_label: { product: "confluence", op: "write", category: "label" },
|
|
215
|
+
confluence_upload_attachment: { product: "confluence", op: "write", category: "attachment" },
|
|
216
|
+
// ---- Confluence destructive ----
|
|
217
|
+
confluence_delete_space: { product: "confluence", op: "destructive", category: "space" },
|
|
218
|
+
confluence_delete_page: { product: "confluence", op: "destructive", category: "page" },
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const COMMENT_TOOLS = new Set([
|
|
222
|
+
"jira_add_comment",
|
|
223
|
+
"jira_reply_comment",
|
|
224
|
+
"jira_edit_comment",
|
|
225
|
+
"confluence_add_comment",
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
const SCOPE_PRESETS = {
|
|
229
|
+
"read-only": (m) => m.op === "read",
|
|
230
|
+
"comments-only": (m, name) => m.op === "read" || COMMENT_TOOLS.has(name),
|
|
231
|
+
"no-destructive": (m) => m.op !== "destructive",
|
|
232
|
+
unrestricted: () => true,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
function checkScope(toolName, instance) {
|
|
236
|
+
const scopes = instance && instance.scopes;
|
|
237
|
+
// Fail-open when instance has no scopes block — preserves existing installs.
|
|
238
|
+
if (!scopes) return { allowed: true };
|
|
239
|
+
|
|
240
|
+
const meta = TOOL_METADATA[toolName];
|
|
241
|
+
// Unknown tool with scopes configured → fail closed.
|
|
242
|
+
if (!meta) {
|
|
243
|
+
return { allowed: false, reason: `Tool '${toolName}' has no metadata; denied under scoped instance '${instance.name}'.` };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Explicit deny wins.
|
|
247
|
+
if (Array.isArray(scopes.deny) && scopes.deny.includes(toolName)) {
|
|
248
|
+
return { allowed: false, reason: `Tool '${toolName}' is in instance '${instance.name}' deny list.` };
|
|
249
|
+
}
|
|
250
|
+
// Explicit allow wins after deny.
|
|
251
|
+
if (Array.isArray(scopes.allow) && scopes.allow.includes(toolName)) {
|
|
252
|
+
return { allowed: true };
|
|
253
|
+
}
|
|
254
|
+
// Preset.
|
|
255
|
+
if (scopes.preset) {
|
|
256
|
+
const presetFn = SCOPE_PRESETS[scopes.preset];
|
|
257
|
+
if (!presetFn) {
|
|
258
|
+
return { allowed: false, reason: `Unknown scope preset '${scopes.preset}' on instance '${instance.name}'.` };
|
|
259
|
+
}
|
|
260
|
+
if (presetFn(meta, toolName)) return { allowed: true };
|
|
261
|
+
return { allowed: false, reason: `Tool '${toolName}' (${meta.op}) not permitted by preset '${scopes.preset}' on '${instance.name}'.` };
|
|
262
|
+
}
|
|
263
|
+
// scopes block present but neither preset nor allow listed → deny.
|
|
264
|
+
return { allowed: false, reason: `Tool '${toolName}' not in instance '${instance.name}' allow list.` };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---- Dry-run resolution ----
|
|
268
|
+
const DRY_RUN_ENV =
|
|
269
|
+
process.env.JIRA_MCP_DRY_RUN === "1" ||
|
|
270
|
+
process.env.JIRA_MCP_DRY_RUN === "true";
|
|
271
|
+
|
|
272
|
+
function resolveDryRun(toolName, args, instance) {
|
|
273
|
+
const meta = TOOL_METADATA[toolName];
|
|
274
|
+
if (!meta || meta.op === "read") return false;
|
|
275
|
+
if (args && args.dryRun === true) return true;
|
|
276
|
+
if (args && args.dryRun === false) return false;
|
|
277
|
+
if (instance && instance.dryRun === true) return true;
|
|
278
|
+
return DRY_RUN_ENV;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Dry-run execution context. fetchJira/fetchConfluence check this and short-
|
|
282
|
+
// circuit. Only the FIRST write in a chain is captured — multi-write tools
|
|
283
|
+
// (e.g. transition with intermediate steps) report just step 1 with a note.
|
|
284
|
+
const DRY_RUN_ABORT = Symbol("jira-mcp.dry-run-abort");
|
|
285
|
+
const _policyCtx = { dryRun: false, plan: null };
|
|
286
|
+
|
|
287
|
+
function recordDryRunCall(call) {
|
|
288
|
+
if (_policyCtx.plan) _policyCtx.plan.push(call);
|
|
289
|
+
throw DRY_RUN_ABORT;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---- Audit log ----
|
|
293
|
+
const AUDIT_LOG_PATH = path.join(
|
|
294
|
+
process.env.HOME || "/tmp",
|
|
295
|
+
".config/jira-mcp/audit.log",
|
|
296
|
+
);
|
|
297
|
+
const AUDIT_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
298
|
+
const AUDIT_LOG_KEEP = 5;
|
|
299
|
+
const SECRET_KEY_PATTERN =
|
|
300
|
+
/^(token|auth|authorization|password|secret|api[_-]?key|file[_-]?content)$/i;
|
|
301
|
+
|
|
302
|
+
function scrubArgs(value) {
|
|
303
|
+
if (value == null) return value;
|
|
304
|
+
if (Array.isArray(value)) return value.map(scrubArgs);
|
|
305
|
+
if (typeof value === "object") {
|
|
306
|
+
const out = {};
|
|
307
|
+
for (const [k, v] of Object.entries(value)) {
|
|
308
|
+
if (SECRET_KEY_PATTERN.test(k)) {
|
|
309
|
+
out[k] = "[REDACTED]";
|
|
310
|
+
} else if (typeof v === "string" && v.length > 4000) {
|
|
311
|
+
out[k] = v.slice(0, 4000) + `…[truncated ${v.length - 4000} chars]`;
|
|
312
|
+
} else {
|
|
313
|
+
out[k] = scrubArgs(v);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function rotateAuditIfNeeded() {
|
|
322
|
+
try {
|
|
323
|
+
const stat = fs.statSync(AUDIT_LOG_PATH);
|
|
324
|
+
if (stat.size < AUDIT_LOG_MAX_BYTES) return;
|
|
325
|
+
} catch {
|
|
326
|
+
return; // file does not exist yet
|
|
327
|
+
}
|
|
328
|
+
// Shift audit.log.N → audit.log.N+1
|
|
329
|
+
for (let i = AUDIT_LOG_KEEP - 1; i >= 1; i--) {
|
|
330
|
+
const src = `${AUDIT_LOG_PATH}.${i}`;
|
|
331
|
+
const dst = `${AUDIT_LOG_PATH}.${i + 1}`;
|
|
332
|
+
try { if (fs.existsSync(src)) fs.renameSync(src, dst); } catch {}
|
|
333
|
+
}
|
|
334
|
+
try { fs.renameSync(AUDIT_LOG_PATH, `${AUDIT_LOG_PATH}.1`); } catch {}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function audit(entry) {
|
|
338
|
+
try {
|
|
339
|
+
if (auditConfig.enabled === false) return;
|
|
340
|
+
const auditDir = path.dirname(AUDIT_LOG_PATH);
|
|
341
|
+
if (!fs.existsSync(auditDir)) fs.mkdirSync(auditDir, { recursive: true });
|
|
342
|
+
rotateAuditIfNeeded();
|
|
343
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n";
|
|
344
|
+
fs.appendFileSync(AUDIT_LOG_PATH, line);
|
|
345
|
+
} catch {
|
|
346
|
+
// Audit failures must not block real writes.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---- Rate limiter (token buckets) ----
|
|
351
|
+
let _clock = () => Date.now();
|
|
352
|
+
function _setClockForTests(fn) { _clock = fn; }
|
|
353
|
+
|
|
354
|
+
// Buckets are populated lazily. Defaults are conservative-but-not-annoying.
|
|
355
|
+
// User can override via config.rateLimit = { enabled: bool, global: N, perInstance: N, destructive: N }.
|
|
356
|
+
const DEFAULT_RL = { enabled: true, global: 60, perInstance: 30, destructive: 5 };
|
|
357
|
+
let _rlConfig = { ...DEFAULT_RL };
|
|
358
|
+
const _buckets = new Map();
|
|
359
|
+
|
|
360
|
+
function _bucket(key, capacity) {
|
|
361
|
+
let b = _buckets.get(key);
|
|
362
|
+
if (!b) {
|
|
363
|
+
b = { tokens: capacity, lastRefill: _clock(), capacity };
|
|
364
|
+
_buckets.set(key, b);
|
|
365
|
+
}
|
|
366
|
+
return b;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function _refill(b, refillPerMin) {
|
|
370
|
+
const now = _clock();
|
|
371
|
+
const elapsed = now - b.lastRefill;
|
|
372
|
+
if (elapsed <= 0) return;
|
|
373
|
+
const refill = (elapsed / 60000) * refillPerMin;
|
|
374
|
+
b.tokens = Math.min(b.capacity, b.tokens + refill);
|
|
375
|
+
b.lastRefill = now;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function checkRateLimit(toolName, instance) {
|
|
379
|
+
if (_rlConfig.enabled === false) return { allowed: true };
|
|
380
|
+
const meta = TOOL_METADATA[toolName];
|
|
381
|
+
if (!meta || meta.op === "read") return { allowed: true };
|
|
382
|
+
|
|
383
|
+
const checks = [
|
|
384
|
+
{ key: "global", capacity: _rlConfig.global },
|
|
385
|
+
{ key: `instance:${instance.name}`, capacity: _rlConfig.perInstance },
|
|
386
|
+
];
|
|
387
|
+
if (meta.op === "destructive") {
|
|
388
|
+
checks.push({ key: "destructive", capacity: _rlConfig.destructive });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const c of checks) {
|
|
392
|
+
const b = _bucket(c.key, c.capacity);
|
|
393
|
+
_refill(b, c.capacity);
|
|
394
|
+
if (b.tokens < 1) {
|
|
395
|
+
const retryAfter = Math.ceil(((1 - b.tokens) / c.capacity) * 60);
|
|
396
|
+
return { allowed: false, bucket: c.key, retryAfter };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Consume from all relevant buckets atomically.
|
|
400
|
+
for (const c of checks) {
|
|
401
|
+
const b = _bucket(c.key, c.capacity);
|
|
402
|
+
b.tokens -= 1;
|
|
403
|
+
}
|
|
404
|
+
return { allowed: true };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function _resetBucketsForTests() {
|
|
408
|
+
_buckets.clear();
|
|
409
|
+
_rlConfig = { ...DEFAULT_RL };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Pick up user overrides from config.
|
|
413
|
+
let auditConfig = { enabled: true };
|
|
414
|
+
if (rawConfig.audit && typeof rawConfig.audit === "object") {
|
|
415
|
+
auditConfig = { ...auditConfig, ...rawConfig.audit };
|
|
416
|
+
}
|
|
417
|
+
if (rawConfig.rateLimit && typeof rawConfig.rateLimit === "object") {
|
|
418
|
+
_rlConfig = { ..._rlConfig, ...rawConfig.rateLimit };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Target extractor for audit log.
|
|
422
|
+
function extractTarget(args) {
|
|
423
|
+
if (!args || typeof args !== "object") return undefined;
|
|
424
|
+
return (
|
|
425
|
+
args.issueKey ||
|
|
426
|
+
args.pageId ||
|
|
427
|
+
args.spaceKey ||
|
|
428
|
+
args.commentId ||
|
|
429
|
+
args.boardId ||
|
|
430
|
+
args.attachmentId ||
|
|
431
|
+
args.accountId ||
|
|
432
|
+
undefined
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Resolve instance for policy purposes (must run BEFORE the dispatch handler).
|
|
437
|
+
// Routing keys vary by tool — issueKey is most common but subtask uses
|
|
438
|
+
// parentKey, link uses inwardIssueKey, create_ticket uses projectKey, and
|
|
439
|
+
// Confluence tools route by spaceKey. If a tool genuinely targets a specific
|
|
440
|
+
// instance via no key (e.g. jira_get_boards), it falls back to the default
|
|
441
|
+
// instance — the user can still override with args.instance.
|
|
442
|
+
function resolveInstanceForTool(toolName, args) {
|
|
443
|
+
if (!args) return defaultInstance;
|
|
444
|
+
if (args.instance) return getInstanceByName(args.instance);
|
|
445
|
+
const keyLike =
|
|
446
|
+
args.issueKey ||
|
|
447
|
+
args.parentKey ||
|
|
448
|
+
args.inwardIssueKey ||
|
|
449
|
+
args.outwardIssueKey;
|
|
450
|
+
if (keyLike) return getInstanceForKey(keyLike);
|
|
451
|
+
const projectLike = args.projectKey || args.spaceKey;
|
|
452
|
+
if (projectLike) return getInstanceForProject(projectLike);
|
|
453
|
+
return defaultInstance;
|
|
454
|
+
}
|
|
455
|
+
|
|
137
456
|
// ============ JIRA FUNCTIONS ============
|
|
138
457
|
|
|
139
458
|
async function fetchJira(endpoint, options = {}, instance = defaultInstance) {
|
|
140
459
|
const { method = "GET", body } = options;
|
|
460
|
+
if (_policyCtx.dryRun && method !== "GET" && method !== "HEAD") {
|
|
461
|
+
recordDryRunCall({ api: "jira", method, endpoint, body, instance: instance.name });
|
|
462
|
+
}
|
|
141
463
|
const headers = {
|
|
142
464
|
Authorization: `Basic ${instance.auth}`,
|
|
143
465
|
Accept: "application/json",
|
|
@@ -162,6 +484,9 @@ async function fetchJira(endpoint, options = {}, instance = defaultInstance) {
|
|
|
162
484
|
|
|
163
485
|
async function fetchJiraAgile(endpoint, options = {}, instance = defaultInstance) {
|
|
164
486
|
const { method = "GET", body } = options;
|
|
487
|
+
if (_policyCtx.dryRun && method !== "GET" && method !== "HEAD") {
|
|
488
|
+
recordDryRunCall({ api: "jira-agile", method, endpoint, body, instance: instance.name });
|
|
489
|
+
}
|
|
165
490
|
const headers = {
|
|
166
491
|
Authorization: `Basic ${instance.auth}`,
|
|
167
492
|
Accept: "application/json",
|
|
@@ -307,6 +632,15 @@ function getConfluenceBaseUrl(instance) {
|
|
|
307
632
|
|
|
308
633
|
async function fetchConfluence(endpoint, options = {}, instance = defaultInstance) {
|
|
309
634
|
const { method = "GET", body, rawBody, contentType, extraHeaders } = options;
|
|
635
|
+
if (_policyCtx.dryRun && method !== "GET" && method !== "HEAD") {
|
|
636
|
+
recordDryRunCall({
|
|
637
|
+
api: "confluence",
|
|
638
|
+
method,
|
|
639
|
+
endpoint,
|
|
640
|
+
body: body !== undefined ? body : rawBody !== undefined ? "<raw body>" : undefined,
|
|
641
|
+
instance: instance.name,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
310
644
|
const headers = {
|
|
311
645
|
Authorization: `Basic ${instance.auth}`,
|
|
312
646
|
Accept: "application/json",
|
|
@@ -435,7 +769,10 @@ function extractText(content, urls = []) {
|
|
|
435
769
|
} else if (node.type === "hardBreak") {
|
|
436
770
|
text += "\n";
|
|
437
771
|
} else if (node.type === "mention") {
|
|
438
|
-
text
|
|
772
|
+
// attrs.text already includes the leading "@" (e.g. "@Rui Branco").
|
|
773
|
+
// Don't prepend a second one.
|
|
774
|
+
const mentionText = node.attrs?.text || "@user";
|
|
775
|
+
text += mentionText.startsWith("@") ? mentionText : `@${mentionText}`;
|
|
439
776
|
} else if (node.type === "mediaGroup" || node.type === "mediaSingle") {
|
|
440
777
|
text += "[image attachment]\n";
|
|
441
778
|
} else if (
|
|
@@ -642,6 +979,54 @@ function autoLinkTextNodes(nodes, instance = defaultInstance) {
|
|
|
642
979
|
return result;
|
|
643
980
|
}
|
|
644
981
|
|
|
982
|
+
// Detect a markdown list item line. Returns {indent, ordered, text} or null.
|
|
983
|
+
// Bullets: -, *, + followed by a space. Ordered: "1." or "1)" followed by a space.
|
|
984
|
+
// The marker must be followed by whitespace so inline emphasis (*italic*) and
|
|
985
|
+
// horizontal rules (***) are NOT misread as list items.
|
|
986
|
+
function listItemMatch(line) {
|
|
987
|
+
const m = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
|
988
|
+
if (!m) return null;
|
|
989
|
+
return { indent: m[1].length, ordered: /\d/.test(m[2]), text: m[3] };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Recursively parse a (possibly nested) markdown list starting at lines[startIdx].
|
|
993
|
+
// baseIndent is the indentation of items at this level; more-indented items are
|
|
994
|
+
// folded into the previous item as a nested list. Returns {node, nextIndex}.
|
|
995
|
+
async function parseListBlock(lines, startIdx, baseIndent, instance) {
|
|
996
|
+
const items = [];
|
|
997
|
+
let i = startIdx;
|
|
998
|
+
let ordered = null;
|
|
999
|
+
while (i < lines.length) {
|
|
1000
|
+
if (!lines[i].trim()) break; // blank line ends the list
|
|
1001
|
+
const m = listItemMatch(lines[i]);
|
|
1002
|
+
if (!m) break;
|
|
1003
|
+
if (m.indent < baseIndent) break; // dedent: belongs to a parent list
|
|
1004
|
+
if (m.indent > baseIndent) {
|
|
1005
|
+
// Deeper indent: nest under the previous item at this level.
|
|
1006
|
+
const sub = await parseListBlock(lines, i, m.indent, instance);
|
|
1007
|
+
if (items.length > 0) {
|
|
1008
|
+
items[items.length - 1].content.push(sub.node);
|
|
1009
|
+
} else {
|
|
1010
|
+
items.push({ type: "listItem", content: [sub.node] });
|
|
1011
|
+
}
|
|
1012
|
+
i = sub.nextIndex;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (ordered === null) ordered = m.ordered; // list type set by first item
|
|
1016
|
+
else if (m.ordered !== ordered) break; // marker switched: caller starts a new list
|
|
1017
|
+
const inlineContent = await parseInlineFormatting(m.text, instance);
|
|
1018
|
+
items.push({
|
|
1019
|
+
type: "listItem",
|
|
1020
|
+
content: [{ type: "paragraph", content: inlineContent }],
|
|
1021
|
+
});
|
|
1022
|
+
i++;
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
node: { type: ordered ? "orderedList" : "bulletList", content: items },
|
|
1026
|
+
nextIndex: i,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
645
1030
|
// Parse text with markdown formatting and @mentions, build ADF content
|
|
646
1031
|
async function buildCommentADF(text, instance = defaultInstance) {
|
|
647
1032
|
// Sanitize: replace em dashes and en dashes with hyphen
|
|
@@ -754,35 +1139,17 @@ async function buildCommentADF(text, instance = defaultInstance) {
|
|
|
754
1139
|
continue;
|
|
755
1140
|
}
|
|
756
1141
|
|
|
757
|
-
// ---
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
content.push({ type: "bulletList", content: listItems });
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// --- Ordered list (1. items) ---
|
|
774
|
-
if (/^\d+\.\s/.test(trimmed)) {
|
|
775
|
-
const listItems = [];
|
|
776
|
-
while (i < lines.length && /^\d+\.\s/.test(lines[i].trim())) {
|
|
777
|
-
const itemText = lines[i].trim().replace(/^\d+\.\s/, "");
|
|
778
|
-
const inlineContent = await parseInlineFormatting(itemText, instance);
|
|
779
|
-
listItems.push({
|
|
780
|
-
type: "listItem",
|
|
781
|
-
content: [{ type: "paragraph", content: inlineContent }],
|
|
782
|
-
});
|
|
783
|
-
i++;
|
|
784
|
-
}
|
|
785
|
-
content.push({ type: "orderedList", content: listItems });
|
|
1142
|
+
// --- Lists (bullet -,*,+ or ordered 1./1)) with nesting ---
|
|
1143
|
+
const listStart = listItemMatch(line);
|
|
1144
|
+
if (listStart) {
|
|
1145
|
+
const { node, nextIndex } = await parseListBlock(
|
|
1146
|
+
lines,
|
|
1147
|
+
i,
|
|
1148
|
+
listStart.indent,
|
|
1149
|
+
instance,
|
|
1150
|
+
);
|
|
1151
|
+
content.push(node);
|
|
1152
|
+
i = nextIndex;
|
|
786
1153
|
continue;
|
|
787
1154
|
}
|
|
788
1155
|
|
|
@@ -792,13 +1159,12 @@ async function buildCommentADF(text, instance = defaultInstance) {
|
|
|
792
1159
|
i < lines.length &&
|
|
793
1160
|
lines[i].trim() &&
|
|
794
1161
|
!lines[i].trim().startsWith("```") &&
|
|
795
|
-
|
|
796
|
-
!lines[i].trim().startsWith("## ") &&
|
|
797
|
-
!lines[i].trim().startsWith("### ") &&
|
|
1162
|
+
!/^#{1,6}\s/.test(lines[i].trim()) &&
|
|
798
1163
|
!lines[i].trim().startsWith("> ") &&
|
|
799
|
-
!lines[i]
|
|
800
|
-
!/^\d+\.\s/.test(lines[i].trim()) &&
|
|
1164
|
+
!listItemMatch(lines[i]) &&
|
|
801
1165
|
!/^-{3,}$/.test(lines[i].trim()) &&
|
|
1166
|
+
!/^\*{3,}$/.test(lines[i].trim()) &&
|
|
1167
|
+
!/^_{3,}$/.test(lines[i].trim()) &&
|
|
802
1168
|
!(lines[i].trim().startsWith("|") && lines[i].trim().endsWith("|"))
|
|
803
1169
|
) {
|
|
804
1170
|
if (paragraphContent.length > 0) paragraphContent.push({ type: "hardBreak" });
|
|
@@ -1662,14 +2028,50 @@ async function getChangelogsBulk(jql, maxResults = 50, instance = defaultInstanc
|
|
|
1662
2028
|
|
|
1663
2029
|
// ============ MCP SERVER ============
|
|
1664
2030
|
|
|
2031
|
+
const SERVER_INSTRUCTIONS = `# jira-mcp usage rules
|
|
2032
|
+
|
|
2033
|
+
- \`instance\` (optional, on most tools): multi-instance setups only. Omit it — it auto-detects from the issue key prefix, else uses the default instance.
|
|
2034
|
+
- \`dryRun\` (optional, on mutating tools): if true, returns the HTTP request that would be sent (method, endpoint, body) without calling Atlassian. Multi-step tools only show the first write.
|
|
2035
|
+
${confluenceMode !== "always" ? "- Confluence tools are hidden until activated: call `confluence_enable` first when a task needs Confluence (pages, spaces, page comments, labels, attachments).\n" : ""}
|
|
2036
|
+
## Writing Jira comments (jira_add_comment / jira_reply_comment / jira_edit_comment)
|
|
2037
|
+
Write one short conversational paragraph: start with @DisplayName of who to notify (NOT [~accountId:...] syntax), say what changed, and end with the next step (e.g. 'Ready for verification.'). Keep it HIGH LEVEL for stakeholders/PO/QA — WHAT changed and the impact, not how it was built. Include technical detail (endpoints, payloads, API contracts) ONLY when the reader is a developer and it makes sense; otherwise leave out code, file paths, branch names, commit hashes, and framework-internal terms. NEVER use em dashes (—) or en dashes (–) — use commas, periods, or rewrite. Avoid bullet lists, headings, and bold unless the update genuinely needs them.
|
|
2038
|
+
|
|
2039
|
+
## Writing descriptions (jira_create_ticket / jira_update_ticket / jira_create_subtask)
|
|
2040
|
+
Structure with Markdown: a one/two-sentence overview, then ## / ### headings (Overview, Details, Acceptance Criteria), '-' bullet lists, numbered steps, tables for structured data, **bold** key terms, fenced \`\`\` blocks for code/payloads. Blank line between blocks. Concise and scannable. Never use em/en dashes.`;
|
|
2041
|
+
|
|
1665
2042
|
const server = new Server(
|
|
1666
2043
|
{ name: "jira-mcp", version: "1.0.0" },
|
|
1667
|
-
{ capabilities: { tools: {} } },
|
|
2044
|
+
{ capabilities: { tools: { listChanged: true } }, instructions: SERVER_INSTRUCTIONS },
|
|
1668
2045
|
);
|
|
1669
2046
|
|
|
1670
|
-
|
|
2047
|
+
// Inject `dryRun` into the schema of any mutating tool so MCP clients with
|
|
2048
|
+
// strict schema validation accept it as a per-call argument.
|
|
2049
|
+
function _injectDryRunSchema(tool) {
|
|
2050
|
+
const meta = TOOL_METADATA[tool.name];
|
|
2051
|
+
if (!meta || meta.op === "read") return tool;
|
|
2052
|
+
const schema = tool.inputSchema || {};
|
|
2053
|
+
const props = schema.properties || {};
|
|
2054
|
+
if (props.dryRun) return tool;
|
|
1671
2055
|
return {
|
|
1672
|
-
|
|
2056
|
+
...tool,
|
|
2057
|
+
inputSchema: {
|
|
2058
|
+
...schema,
|
|
2059
|
+
properties: {
|
|
2060
|
+
...props,
|
|
2061
|
+
dryRun: {
|
|
2062
|
+
type: "boolean",
|
|
2063
|
+
description: "Preview: return the would-be HTTP request without executing.",
|
|
2064
|
+
},
|
|
2065
|
+
},
|
|
2066
|
+
},
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// Additive formatting guidance moved to SERVER_INSTRUCTIONS to reduce per-tool
|
|
2071
|
+
// token cost. Field descriptions retain their short base text only.
|
|
2072
|
+
|
|
2073
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2074
|
+
const rawTools = [
|
|
1673
2075
|
{
|
|
1674
2076
|
name: "jira_get_myself",
|
|
1675
2077
|
description:
|
|
@@ -1679,7 +2081,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1679
2081
|
properties: {
|
|
1680
2082
|
instance: {
|
|
1681
2083
|
type: "string",
|
|
1682
|
-
description: "Instance name (
|
|
2084
|
+
description: "Instance name (optional).",
|
|
1683
2085
|
},
|
|
1684
2086
|
},
|
|
1685
2087
|
required: [],
|
|
@@ -1707,7 +2109,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1707
2109
|
},
|
|
1708
2110
|
instance: {
|
|
1709
2111
|
type: "string",
|
|
1710
|
-
description: "Instance name
|
|
2112
|
+
description: "Instance name (optional).",
|
|
1711
2113
|
},
|
|
1712
2114
|
},
|
|
1713
2115
|
required: ["issueKey"],
|
|
@@ -1733,7 +2135,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1733
2135
|
},
|
|
1734
2136
|
instance: {
|
|
1735
2137
|
type: "string",
|
|
1736
|
-
description: "Instance name (
|
|
2138
|
+
description: "Instance name (optional).",
|
|
1737
2139
|
},
|
|
1738
2140
|
},
|
|
1739
2141
|
required: ["jql"],
|
|
@@ -1742,7 +2144,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1742
2144
|
{
|
|
1743
2145
|
name: "jira_add_comment",
|
|
1744
2146
|
description:
|
|
1745
|
-
"Add a comment to a Jira ticket.
|
|
2147
|
+
"Add a comment to a Jira ticket. Follow the server instructions' comment style rules.",
|
|
1746
2148
|
inputSchema: {
|
|
1747
2149
|
type: "object",
|
|
1748
2150
|
properties: {
|
|
@@ -1750,10 +2152,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1750
2152
|
type: "string",
|
|
1751
2153
|
description: "The Jira issue key (e.g., MODS-123)",
|
|
1752
2154
|
},
|
|
1753
|
-
comment: {
|
|
2155
|
+
comment: {
|
|
2156
|
+
type: "string",
|
|
2157
|
+
description: "The comment text to add.",
|
|
2158
|
+
},
|
|
1754
2159
|
instance: {
|
|
1755
2160
|
type: "string",
|
|
1756
|
-
description: "Instance name
|
|
2161
|
+
description: "Instance name (optional).",
|
|
1757
2162
|
},
|
|
1758
2163
|
},
|
|
1759
2164
|
required: ["issueKey", "comment"],
|
|
@@ -1775,10 +2180,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1775
2180
|
description:
|
|
1776
2181
|
"The ID of the comment to reply to. Use jira_get_ticket to see comments and their IDs.",
|
|
1777
2182
|
},
|
|
1778
|
-
reply: {
|
|
2183
|
+
reply: {
|
|
2184
|
+
type: "string",
|
|
2185
|
+
description: "The reply text.",
|
|
2186
|
+
},
|
|
1779
2187
|
instance: {
|
|
1780
2188
|
type: "string",
|
|
1781
|
-
description: "Instance name
|
|
2189
|
+
description: "Instance name (optional).",
|
|
1782
2190
|
},
|
|
1783
2191
|
},
|
|
1784
2192
|
required: ["issueKey", "commentId", "reply"],
|
|
@@ -1787,7 +2195,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1787
2195
|
{
|
|
1788
2196
|
name: "jira_edit_comment",
|
|
1789
2197
|
description:
|
|
1790
|
-
"Edit an existing comment on a Jira ticket
|
|
2198
|
+
"Edit an existing comment on a Jira ticket (replaces the text). Follow the server instructions' comment style rules.",
|
|
1791
2199
|
inputSchema: {
|
|
1792
2200
|
type: "object",
|
|
1793
2201
|
properties: {
|
|
@@ -1800,10 +2208,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1800
2208
|
description:
|
|
1801
2209
|
"The ID of the comment to edit. Use jira_get_ticket to see comments and their IDs.",
|
|
1802
2210
|
},
|
|
1803
|
-
comment: {
|
|
2211
|
+
comment: {
|
|
2212
|
+
type: "string",
|
|
2213
|
+
description: "The new comment text.",
|
|
2214
|
+
},
|
|
1804
2215
|
instance: {
|
|
1805
2216
|
type: "string",
|
|
1806
|
-
description: "Instance name
|
|
2217
|
+
description: "Instance name (optional).",
|
|
1807
2218
|
},
|
|
1808
2219
|
},
|
|
1809
2220
|
required: ["issueKey", "commentId", "comment"],
|
|
@@ -1827,7 +2238,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1827
2238
|
},
|
|
1828
2239
|
instance: {
|
|
1829
2240
|
type: "string",
|
|
1830
|
-
description: "Instance name
|
|
2241
|
+
description: "Instance name (optional).",
|
|
1831
2242
|
},
|
|
1832
2243
|
},
|
|
1833
2244
|
required: ["issueKey", "commentId"],
|
|
@@ -1856,7 +2267,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1856
2267
|
},
|
|
1857
2268
|
instance: {
|
|
1858
2269
|
type: "string",
|
|
1859
|
-
description: "Instance name
|
|
2270
|
+
description: "Instance name (optional).",
|
|
1860
2271
|
},
|
|
1861
2272
|
},
|
|
1862
2273
|
required: ["issueKey"],
|
|
@@ -1917,7 +2328,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1917
2328
|
},
|
|
1918
2329
|
instance: {
|
|
1919
2330
|
type: "string",
|
|
1920
|
-
description: "Instance name
|
|
2331
|
+
description: "Instance name (optional).",
|
|
1921
2332
|
},
|
|
1922
2333
|
},
|
|
1923
2334
|
required: ["issueKey"],
|
|
@@ -1941,7 +2352,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1941
2352
|
},
|
|
1942
2353
|
instance: {
|
|
1943
2354
|
type: "string",
|
|
1944
|
-
description: "Instance name (
|
|
2355
|
+
description: "Instance name (optional).",
|
|
1945
2356
|
},
|
|
1946
2357
|
},
|
|
1947
2358
|
required: ["query"],
|
|
@@ -1971,7 +2382,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1971
2382
|
},
|
|
1972
2383
|
instance: {
|
|
1973
2384
|
type: "string",
|
|
1974
|
-
description: "Instance name (
|
|
2385
|
+
description: "Instance name (optional).",
|
|
1975
2386
|
},
|
|
1976
2387
|
},
|
|
1977
2388
|
required: [],
|
|
@@ -2063,7 +2474,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2063
2474
|
},
|
|
2064
2475
|
instance: {
|
|
2065
2476
|
type: "string",
|
|
2066
|
-
description: "Instance name
|
|
2477
|
+
description: "Instance name (optional).",
|
|
2067
2478
|
},
|
|
2068
2479
|
},
|
|
2069
2480
|
required: [],
|
|
@@ -2090,7 +2501,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2090
2501
|
},
|
|
2091
2502
|
description: {
|
|
2092
2503
|
type: "string",
|
|
2093
|
-
description:
|
|
2504
|
+
description:
|
|
2505
|
+
"The issue description text. Supports @mentions via @DisplayName.",
|
|
2094
2506
|
},
|
|
2095
2507
|
assignee: {
|
|
2096
2508
|
type: "string",
|
|
@@ -2133,7 +2545,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2133
2545
|
},
|
|
2134
2546
|
instance: {
|
|
2135
2547
|
type: "string",
|
|
2136
|
-
description: "Instance name
|
|
2548
|
+
description: "Instance name (optional).",
|
|
2137
2549
|
},
|
|
2138
2550
|
},
|
|
2139
2551
|
required: ["projectKey", "issueType", "summary"],
|
|
@@ -2156,7 +2568,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2156
2568
|
},
|
|
2157
2569
|
description: {
|
|
2158
2570
|
type: "string",
|
|
2159
|
-
description:
|
|
2571
|
+
description:
|
|
2572
|
+
"The subtask description text. Supports @mentions via @DisplayName.",
|
|
2160
2573
|
},
|
|
2161
2574
|
assignee: {
|
|
2162
2575
|
type: "string",
|
|
@@ -2177,7 +2590,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2177
2590
|
},
|
|
2178
2591
|
instance: {
|
|
2179
2592
|
type: "string",
|
|
2180
|
-
description: "Instance name
|
|
2593
|
+
description: "Instance name (optional).",
|
|
2181
2594
|
},
|
|
2182
2595
|
},
|
|
2183
2596
|
required: ["parentKey", "summary"],
|
|
@@ -2208,7 +2621,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2208
2621
|
},
|
|
2209
2622
|
instance: {
|
|
2210
2623
|
type: "string",
|
|
2211
|
-
description: "Instance name
|
|
2624
|
+
description: "Instance name (optional).",
|
|
2212
2625
|
},
|
|
2213
2626
|
},
|
|
2214
2627
|
required: ["inwardIssueKey", "outwardIssueKey", "linkType"],
|
|
@@ -2231,7 +2644,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2231
2644
|
},
|
|
2232
2645
|
instance: {
|
|
2233
2646
|
type: "string",
|
|
2234
|
-
description: "Instance name
|
|
2647
|
+
description: "Instance name (optional).",
|
|
2235
2648
|
},
|
|
2236
2649
|
},
|
|
2237
2650
|
required: ["issueKey"],
|
|
@@ -2262,7 +2675,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2262
2675
|
},
|
|
2263
2676
|
instance: {
|
|
2264
2677
|
type: "string",
|
|
2265
|
-
description: "Instance name
|
|
2678
|
+
description: "Instance name (optional).",
|
|
2266
2679
|
},
|
|
2267
2680
|
},
|
|
2268
2681
|
required: ["issueKey"],
|
|
@@ -2277,7 +2690,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2277
2690
|
properties: {
|
|
2278
2691
|
instance: {
|
|
2279
2692
|
type: "string",
|
|
2280
|
-
description: "Instance name (
|
|
2693
|
+
description: "Instance name (optional).",
|
|
2281
2694
|
},
|
|
2282
2695
|
},
|
|
2283
2696
|
required: [],
|
|
@@ -2296,7 +2709,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2296
2709
|
},
|
|
2297
2710
|
instance: {
|
|
2298
2711
|
type: "string",
|
|
2299
|
-
description: "Instance name
|
|
2712
|
+
description: "Instance name (optional).",
|
|
2300
2713
|
},
|
|
2301
2714
|
},
|
|
2302
2715
|
required: ["issueKey"],
|
|
@@ -2319,7 +2732,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2319
2732
|
},
|
|
2320
2733
|
instance: {
|
|
2321
2734
|
type: "string",
|
|
2322
|
-
description: "Instance name
|
|
2735
|
+
description: "Instance name (optional).",
|
|
2323
2736
|
},
|
|
2324
2737
|
},
|
|
2325
2738
|
required: ["issueKey", "linkedIssueKey"],
|
|
@@ -2346,7 +2759,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2346
2759
|
},
|
|
2347
2760
|
instance: {
|
|
2348
2761
|
type: "string",
|
|
2349
|
-
description: "Instance name
|
|
2762
|
+
description: "Instance name (optional).",
|
|
2350
2763
|
},
|
|
2351
2764
|
},
|
|
2352
2765
|
required: [],
|
|
@@ -2369,7 +2782,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2369
2782
|
},
|
|
2370
2783
|
instance: {
|
|
2371
2784
|
type: "string",
|
|
2372
|
-
description: "Instance name
|
|
2785
|
+
description: "Instance name (optional).",
|
|
2373
2786
|
},
|
|
2374
2787
|
},
|
|
2375
2788
|
required: ["issueKey", "accountId"],
|
|
@@ -2392,7 +2805,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2392
2805
|
},
|
|
2393
2806
|
instance: {
|
|
2394
2807
|
type: "string",
|
|
2395
|
-
description: "Instance name
|
|
2808
|
+
description: "Instance name (optional).",
|
|
2396
2809
|
},
|
|
2397
2810
|
},
|
|
2398
2811
|
required: ["issueKey", "accountId"],
|
|
@@ -2423,7 +2836,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2423
2836
|
},
|
|
2424
2837
|
instance: {
|
|
2425
2838
|
type: "string",
|
|
2426
|
-
description: "Instance name
|
|
2839
|
+
description: "Instance name (optional).",
|
|
2427
2840
|
},
|
|
2428
2841
|
},
|
|
2429
2842
|
required: ["issueKey", "timeSpent"],
|
|
@@ -2442,7 +2855,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2442
2855
|
},
|
|
2443
2856
|
instance: {
|
|
2444
2857
|
type: "string",
|
|
2445
|
-
description: "Instance name
|
|
2858
|
+
description: "Instance name (optional).",
|
|
2446
2859
|
},
|
|
2447
2860
|
},
|
|
2448
2861
|
required: ["issueKey"],
|
|
@@ -2473,7 +2886,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2473
2886
|
},
|
|
2474
2887
|
instance: {
|
|
2475
2888
|
type: "string",
|
|
2476
|
-
description: "Instance name
|
|
2889
|
+
description: "Instance name (optional).",
|
|
2477
2890
|
},
|
|
2478
2891
|
},
|
|
2479
2892
|
required: ["issueKey"],
|
|
@@ -2497,7 +2910,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2497
2910
|
},
|
|
2498
2911
|
instance: {
|
|
2499
2912
|
type: "string",
|
|
2500
|
-
description: "Instance name (
|
|
2913
|
+
description: "Instance name (optional).",
|
|
2501
2914
|
},
|
|
2502
2915
|
},
|
|
2503
2916
|
required: ["sprintId", "issueKeys"],
|
|
@@ -2520,7 +2933,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2520
2933
|
},
|
|
2521
2934
|
instance: {
|
|
2522
2935
|
type: "string",
|
|
2523
|
-
description: "Instance name (
|
|
2936
|
+
description: "Instance name (optional).",
|
|
2524
2937
|
},
|
|
2525
2938
|
},
|
|
2526
2939
|
required: ["boardId"],
|
|
@@ -2547,7 +2960,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2547
2960
|
},
|
|
2548
2961
|
instance: {
|
|
2549
2962
|
type: "string",
|
|
2550
|
-
description: "Instance name (
|
|
2963
|
+
description: "Instance name (optional).",
|
|
2551
2964
|
},
|
|
2552
2965
|
},
|
|
2553
2966
|
required: [],
|
|
@@ -2566,7 +2979,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2566
2979
|
},
|
|
2567
2980
|
instance: {
|
|
2568
2981
|
type: "string",
|
|
2569
|
-
description: "Instance name
|
|
2982
|
+
description: "Instance name (optional).",
|
|
2570
2983
|
},
|
|
2571
2984
|
},
|
|
2572
2985
|
required: ["projectKey"],
|
|
@@ -2581,7 +2994,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2581
2994
|
properties: {
|
|
2582
2995
|
instance: {
|
|
2583
2996
|
type: "string",
|
|
2584
|
-
description: "Instance name (
|
|
2997
|
+
description: "Instance name (optional).",
|
|
2585
2998
|
},
|
|
2586
2999
|
},
|
|
2587
3000
|
required: [],
|
|
@@ -2600,7 +3013,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2600
3013
|
},
|
|
2601
3014
|
instance: {
|
|
2602
3015
|
type: "string",
|
|
2603
|
-
description: "Instance name
|
|
3016
|
+
description: "Instance name (optional).",
|
|
2604
3017
|
},
|
|
2605
3018
|
},
|
|
2606
3019
|
required: ["projectKey"],
|
|
@@ -2619,7 +3032,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2619
3032
|
},
|
|
2620
3033
|
instance: {
|
|
2621
3034
|
type: "string",
|
|
2622
|
-
description: "Instance name
|
|
3035
|
+
description: "Instance name (optional).",
|
|
2623
3036
|
},
|
|
2624
3037
|
},
|
|
2625
3038
|
required: ["projectKey"],
|
|
@@ -2634,7 +3047,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2634
3047
|
properties: {
|
|
2635
3048
|
instance: {
|
|
2636
3049
|
type: "string",
|
|
2637
|
-
description: "Instance name
|
|
3050
|
+
description: "Instance name (optional).",
|
|
2638
3051
|
},
|
|
2639
3052
|
},
|
|
2640
3053
|
required: [],
|
|
@@ -2648,7 +3061,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2648
3061
|
type: "object",
|
|
2649
3062
|
properties: {
|
|
2650
3063
|
spaceKey: { type: "string", description: "Space key (e.g., 'ENG')." },
|
|
2651
|
-
instance: { type: "string", description: "Instance name
|
|
3064
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2652
3065
|
},
|
|
2653
3066
|
required: ["spaceKey"],
|
|
2654
3067
|
},
|
|
@@ -2663,7 +3076,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2663
3076
|
spaceKey: { type: "string", description: "Unique space key (letters/numbers). Will be uppercased." },
|
|
2664
3077
|
name: { type: "string", description: "Human-readable space name." },
|
|
2665
3078
|
description: { type: "string", description: "Optional plain-text description." },
|
|
2666
|
-
instance: { type: "string", description: "Instance name
|
|
3079
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2667
3080
|
},
|
|
2668
3081
|
required: ["spaceKey", "name"],
|
|
2669
3082
|
},
|
|
@@ -2678,7 +3091,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2678
3091
|
spaceKey: { type: "string", description: "Space key to update." },
|
|
2679
3092
|
name: { type: "string", description: "New space name." },
|
|
2680
3093
|
description: { type: "string", description: "New plain-text description." },
|
|
2681
|
-
instance: { type: "string", description: "Instance name
|
|
3094
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2682
3095
|
},
|
|
2683
3096
|
required: ["spaceKey"],
|
|
2684
3097
|
},
|
|
@@ -2691,7 +3104,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2691
3104
|
type: "object",
|
|
2692
3105
|
properties: {
|
|
2693
3106
|
spaceKey: { type: "string", description: "Space key to delete." },
|
|
2694
|
-
instance: { type: "string", description: "Instance name
|
|
3107
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2695
3108
|
},
|
|
2696
3109
|
required: ["spaceKey"],
|
|
2697
3110
|
},
|
|
@@ -2707,7 +3120,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2707
3120
|
spaceKey: { type: "string", description: "Optional space key to scope the search." },
|
|
2708
3121
|
type: { type: "string", description: "Content type (default: page). e.g. page, blogpost, comment, attachment." },
|
|
2709
3122
|
limit: { type: "number", description: "Max results (default: 25)." },
|
|
2710
|
-
instance: { type: "string", description: "Instance name
|
|
3123
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2711
3124
|
},
|
|
2712
3125
|
required: ["query"],
|
|
2713
3126
|
},
|
|
@@ -2722,7 +3135,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2722
3135
|
spaceKey: { type: "string", description: "Optional space key to scope the query." },
|
|
2723
3136
|
sinceDays: { type: "number", description: "Days back from now (default: 30)." },
|
|
2724
3137
|
limit: { type: "number", description: "Max results (default: 25)." },
|
|
2725
|
-
instance: { type: "string", description: "Instance name
|
|
3138
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2726
3139
|
},
|
|
2727
3140
|
required: [],
|
|
2728
3141
|
},
|
|
@@ -2736,7 +3149,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2736
3149
|
properties: {
|
|
2737
3150
|
spaceKey: { type: "string", description: "Space key (required)." },
|
|
2738
3151
|
limit: { type: "number", description: "Max results (default: 50)." },
|
|
2739
|
-
instance: { type: "string", description: "Instance name
|
|
3152
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2740
3153
|
},
|
|
2741
3154
|
required: ["spaceKey"],
|
|
2742
3155
|
},
|
|
@@ -2750,7 +3163,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2750
3163
|
properties: {
|
|
2751
3164
|
pageId: { type: "string", description: "Parent page ID (required)." },
|
|
2752
3165
|
limit: { type: "number", description: "Max results (default: 50)." },
|
|
2753
|
-
instance: { type: "string", description: "Instance name
|
|
3166
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2754
3167
|
},
|
|
2755
3168
|
required: ["pageId"],
|
|
2756
3169
|
},
|
|
@@ -2764,7 +3177,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2764
3177
|
properties: {
|
|
2765
3178
|
pageId: { type: "string", description: "Page ID (required)." },
|
|
2766
3179
|
includeAdf: { type: "boolean", description: "Fetch ADF body via v2 API and merge (default: true)." },
|
|
2767
|
-
instance: { type: "string", description: "Instance name
|
|
3180
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2768
3181
|
},
|
|
2769
3182
|
required: ["pageId"],
|
|
2770
3183
|
},
|
|
@@ -2778,7 +3191,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2778
3191
|
properties: {
|
|
2779
3192
|
pageId: { type: "string", description: "Page ID (required)." },
|
|
2780
3193
|
limit: { type: "number", description: "Max results (default: 50)." },
|
|
2781
|
-
instance: { type: "string", description: "Instance name
|
|
3194
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2782
3195
|
},
|
|
2783
3196
|
required: ["pageId"],
|
|
2784
3197
|
},
|
|
@@ -2793,7 +3206,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2793
3206
|
pageId: { type: "string", description: "Page ID (required)." },
|
|
2794
3207
|
body: { type: "string", description: "Comment body (plain text by default, or storage XHTML if format=storage)." },
|
|
2795
3208
|
format: { type: "string", description: "'text' (default) or 'storage'." },
|
|
2796
|
-
instance: { type: "string", description: "Instance name
|
|
3209
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2797
3210
|
},
|
|
2798
3211
|
required: ["pageId", "body"],
|
|
2799
3212
|
},
|
|
@@ -2809,7 +3222,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2809
3222
|
title: { type: "string", description: "New page title (optional — retains current if omitted)." },
|
|
2810
3223
|
body: { type: "string", description: "New body as storage XHTML (optional — retains current if omitted)." },
|
|
2811
3224
|
version: { type: "number", description: "Explicit new version number. If omitted, current+1 is computed." },
|
|
2812
|
-
instance: { type: "string", description: "Instance name
|
|
3225
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2813
3226
|
},
|
|
2814
3227
|
required: ["pageId"],
|
|
2815
3228
|
},
|
|
@@ -2825,7 +3238,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2825
3238
|
title: { type: "string", description: "Page title (required)." },
|
|
2826
3239
|
body: { type: "string", description: "Storage-format XHTML body (required)." },
|
|
2827
3240
|
parentId: { type: "string", description: "Optional parent page ID to nest under." },
|
|
2828
|
-
instance: { type: "string", description: "Instance name
|
|
3241
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2829
3242
|
},
|
|
2830
3243
|
required: ["spaceKey", "title", "body"],
|
|
2831
3244
|
},
|
|
@@ -2837,7 +3250,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2837
3250
|
type: "object",
|
|
2838
3251
|
properties: {
|
|
2839
3252
|
pageId: { type: "string", description: "Page ID (required)." },
|
|
2840
|
-
instance: { type: "string", description: "Instance name
|
|
3253
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2841
3254
|
},
|
|
2842
3255
|
required: ["pageId"],
|
|
2843
3256
|
},
|
|
@@ -2849,7 +3262,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2849
3262
|
type: "object",
|
|
2850
3263
|
properties: {
|
|
2851
3264
|
pageId: { type: "string", description: "Page ID (required)." },
|
|
2852
|
-
instance: { type: "string", description: "Instance name
|
|
3265
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2853
3266
|
},
|
|
2854
3267
|
required: ["pageId"],
|
|
2855
3268
|
},
|
|
@@ -2862,7 +3275,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2862
3275
|
properties: {
|
|
2863
3276
|
pageId: { type: "string", description: "Page ID (required)." },
|
|
2864
3277
|
label: { type: "string", description: "Label name (required)." },
|
|
2865
|
-
instance: { type: "string", description: "Instance name
|
|
3278
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2866
3279
|
},
|
|
2867
3280
|
required: ["pageId", "label"],
|
|
2868
3281
|
},
|
|
@@ -2875,7 +3288,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2875
3288
|
properties: {
|
|
2876
3289
|
pageId: { type: "string", description: "Page ID (required)." },
|
|
2877
3290
|
label: { type: "string", description: "Label name (required)." },
|
|
2878
|
-
instance: { type: "string", description: "Instance name
|
|
3291
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2879
3292
|
},
|
|
2880
3293
|
required: ["pageId", "label"],
|
|
2881
3294
|
},
|
|
@@ -2888,7 +3301,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2888
3301
|
properties: {
|
|
2889
3302
|
pageId: { type: "string", description: "Page ID (required)." },
|
|
2890
3303
|
limit: { type: "number", description: "Max results (default: 50)." },
|
|
2891
|
-
instance: { type: "string", description: "Instance name
|
|
3304
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2892
3305
|
},
|
|
2893
3306
|
required: ["pageId"],
|
|
2894
3307
|
},
|
|
@@ -2905,7 +3318,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2905
3318
|
fileContent: { type: "string", description: "Base64-encoded file content (requires fileName)." },
|
|
2906
3319
|
fileName: { type: "string", description: "File name (required with fileContent; optional with filePath)." },
|
|
2907
3320
|
comment: { type: "string", description: "Optional attachment comment." },
|
|
2908
|
-
instance: { type: "string", description: "Instance name
|
|
3321
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2909
3322
|
},
|
|
2910
3323
|
required: ["pageId"],
|
|
2911
3324
|
},
|
|
@@ -2919,19 +3332,153 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2919
3332
|
properties: {
|
|
2920
3333
|
pageId: { type: "string", description: "Page ID (required)." },
|
|
2921
3334
|
filename: { type: "string", description: "Attachment filename (required)." },
|
|
2922
|
-
instance: { type: "string", description: "Instance name
|
|
3335
|
+
instance: { type: "string", description: "Instance name (optional)." },
|
|
2923
3336
|
},
|
|
2924
3337
|
required: ["pageId", "filename"],
|
|
2925
3338
|
},
|
|
2926
3339
|
},
|
|
2927
|
-
]
|
|
2928
|
-
|
|
3340
|
+
];
|
|
3341
|
+
let tools = rawTools;
|
|
3342
|
+
if (!confluenceActive) {
|
|
3343
|
+
tools = rawTools.filter(t => !t.name.startsWith("confluence_"));
|
|
3344
|
+
tools.push({
|
|
3345
|
+
name: "confluence_enable",
|
|
3346
|
+
description: "Activate the Confluence tool set (pages, spaces, page comments, labels, attachments, search). Call this first when a task needs Confluence; the confluence_* tools then become available. Activation only changes tool visibility for this server process; per-instance scopes still apply to every Confluence operation.",
|
|
3347
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3350
|
+
return { tools: tools.map(_injectDryRunSchema) };
|
|
2929
3351
|
});
|
|
2930
3352
|
|
|
3353
|
+
async function runWithPolicy(name, args, handler) {
|
|
3354
|
+
const meta = TOOL_METADATA[name];
|
|
3355
|
+
const inst = resolveInstanceForTool(name, args);
|
|
3356
|
+
|
|
3357
|
+
// 1. Scope check
|
|
3358
|
+
const scopeResult = checkScope(name, inst);
|
|
3359
|
+
if (!scopeResult.allowed) {
|
|
3360
|
+
if (meta && meta.op !== "read") {
|
|
3361
|
+
audit({
|
|
3362
|
+
tool: name,
|
|
3363
|
+
instance: inst.name,
|
|
3364
|
+
target: extractTarget(args),
|
|
3365
|
+
args: scrubArgs(args),
|
|
3366
|
+
success: false,
|
|
3367
|
+
deniedBy: "scope",
|
|
3368
|
+
reason: scopeResult.reason,
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
return {
|
|
3372
|
+
content: [{ type: "text", text: `Scope denied: ${scopeResult.reason}` }],
|
|
3373
|
+
isError: true,
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// 2. Resolve dry-run intent
|
|
3378
|
+
const dryRun = resolveDryRun(name, args, inst);
|
|
3379
|
+
|
|
3380
|
+
// 3. Rate limit — real writes only (dry-runs do not consume quota)
|
|
3381
|
+
if (!dryRun && meta && (meta.op === "write" || meta.op === "destructive")) {
|
|
3382
|
+
const rl = checkRateLimit(name, inst);
|
|
3383
|
+
if (!rl.allowed) {
|
|
3384
|
+
audit({
|
|
3385
|
+
tool: name,
|
|
3386
|
+
instance: inst.name,
|
|
3387
|
+
target: extractTarget(args),
|
|
3388
|
+
args: scrubArgs(args),
|
|
3389
|
+
success: false,
|
|
3390
|
+
deniedBy: "rate-limit",
|
|
3391
|
+
bucket: rl.bucket,
|
|
3392
|
+
retryAfter: rl.retryAfter,
|
|
3393
|
+
});
|
|
3394
|
+
return {
|
|
3395
|
+
content: [{
|
|
3396
|
+
type: "text",
|
|
3397
|
+
text: `Rate limit exceeded (bucket: ${rl.bucket}). Retry after ${rl.retryAfter}s.`,
|
|
3398
|
+
}],
|
|
3399
|
+
isError: true,
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
// 4. Execute under policy context
|
|
3405
|
+
_policyCtx.dryRun = dryRun;
|
|
3406
|
+
_policyCtx.plan = dryRun ? [] : null;
|
|
3407
|
+
try {
|
|
3408
|
+
const result = await handler();
|
|
3409
|
+
if (meta && meta.op !== "read") {
|
|
3410
|
+
audit({
|
|
3411
|
+
tool: name,
|
|
3412
|
+
instance: inst.name,
|
|
3413
|
+
target: extractTarget(args),
|
|
3414
|
+
args: scrubArgs(args),
|
|
3415
|
+
dryRun,
|
|
3416
|
+
success: true,
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
return result;
|
|
3420
|
+
} catch (error) {
|
|
3421
|
+
if (error === DRY_RUN_ABORT) {
|
|
3422
|
+
const plan = _policyCtx.plan || [];
|
|
3423
|
+
audit({
|
|
3424
|
+
tool: name,
|
|
3425
|
+
instance: inst.name,
|
|
3426
|
+
target: extractTarget(args),
|
|
3427
|
+
args: scrubArgs(args),
|
|
3428
|
+
dryRun: true,
|
|
3429
|
+
success: true,
|
|
3430
|
+
status: "dry-run",
|
|
3431
|
+
plan,
|
|
3432
|
+
});
|
|
3433
|
+
return {
|
|
3434
|
+
content: [{
|
|
3435
|
+
type: "text",
|
|
3436
|
+
text: JSON.stringify({
|
|
3437
|
+
dryRun: true,
|
|
3438
|
+
note: "Plan shows the first HTTP write only. Multi-step tools (e.g. jira_transition with intermediate steps) execute the remaining writes when run for real.",
|
|
3439
|
+
plan,
|
|
3440
|
+
}, null, 2),
|
|
3441
|
+
}],
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
if (meta && meta.op !== "read") {
|
|
3445
|
+
audit({
|
|
3446
|
+
tool: name,
|
|
3447
|
+
instance: inst.name,
|
|
3448
|
+
target: extractTarget(args),
|
|
3449
|
+
args: scrubArgs(args),
|
|
3450
|
+
dryRun,
|
|
3451
|
+
success: false,
|
|
3452
|
+
error: error.message,
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
return {
|
|
3456
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
3457
|
+
isError: true,
|
|
3458
|
+
};
|
|
3459
|
+
} finally {
|
|
3460
|
+
_policyCtx.dryRun = false;
|
|
3461
|
+
_policyCtx.plan = null;
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
|
|
2931
3465
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2932
3466
|
const { name, arguments: args } = request.params;
|
|
2933
3467
|
|
|
2934
|
-
|
|
3468
|
+
if (name === "confluence_enable") {
|
|
3469
|
+
const wasActive = confluenceActive;
|
|
3470
|
+
confluenceActive = true;
|
|
3471
|
+
if (!wasActive) server.sendToolListChanged().catch((e) => console.error("jira-mcp: tools/list_changed notification failed:", e?.message || e));
|
|
3472
|
+
return { content: [{ type: "text", text: wasActive ? "Confluence tools were already active." : "Confluence tools activated — the confluence_* tools are now available." }] };
|
|
3473
|
+
}
|
|
3474
|
+
// A client with a cached tool list may call a confluence_* tool directly
|
|
3475
|
+
// while hidden — auto-activate instead of failing.
|
|
3476
|
+
if (!confluenceActive && TOOL_METADATA[name]?.product === "confluence" && name !== "confluence_enable") {
|
|
3477
|
+
confluenceActive = true;
|
|
3478
|
+
server.sendToolListChanged().catch((e) => console.error("jira-mcp: tools/list_changed notification failed:", e?.message || e));
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
return runWithPolicy(name, args, async () => {
|
|
2935
3482
|
if (name === "jira_get_myself") {
|
|
2936
3483
|
const inst = getInstanceByName(args.instance);
|
|
2937
3484
|
const result = await fetchJira("/myself", {}, inst);
|
|
@@ -3488,11 +4035,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3488
4035
|
}
|
|
3489
4036
|
}
|
|
3490
4037
|
|
|
3491
|
-
// Save without the computed auth field
|
|
3492
|
-
|
|
4038
|
+
// Save without the computed auth field. Preserve any safety/observability
|
|
4039
|
+
// fields the user has configured on the existing instance (scopes, dryRun,
|
|
4040
|
+
// confluenceBaseUrl) so they survive a re-run of jira_add_instance.
|
|
4041
|
+
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
4042
|
+
const prevSaved = savedIdx >= 0 ? savedConfig.instances[savedIdx] : {};
|
|
4043
|
+
const toSave = {
|
|
4044
|
+
...prevSaved,
|
|
4045
|
+
name: instName,
|
|
4046
|
+
email,
|
|
4047
|
+
token,
|
|
4048
|
+
baseUrl,
|
|
4049
|
+
projects,
|
|
4050
|
+
};
|
|
3493
4051
|
if (defaultTeam) toSave.defaultTeam = defaultTeam;
|
|
3494
4052
|
if (Object.keys(projectTeams).length > 0) toSave.projectTeams = projectTeams;
|
|
3495
|
-
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
3496
4053
|
if (savedIdx >= 0) {
|
|
3497
4054
|
savedConfig.instances[savedIdx] = toSave;
|
|
3498
4055
|
} else {
|
|
@@ -3828,6 +4385,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3828
4385
|
Buffer.from(footer),
|
|
3829
4386
|
]);
|
|
3830
4387
|
|
|
4388
|
+
if (_policyCtx.dryRun) {
|
|
4389
|
+
recordDryRunCall({
|
|
4390
|
+
api: "jira",
|
|
4391
|
+
method: "POST",
|
|
4392
|
+
endpoint: `/issue/${args.issueKey}/attachments`,
|
|
4393
|
+
body: `<multipart upload: ${fileName}, ${fileBuffer.length} bytes>`,
|
|
4394
|
+
instance: inst.name,
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
|
|
3831
4398
|
const response = await fetch(
|
|
3832
4399
|
`${inst.baseUrl}/rest/api/3/issue/${args.issueKey}/attachments`,
|
|
3833
4400
|
{
|
|
@@ -4718,6 +5285,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4718
5285
|
const bodyBuffer = Buffer.concat(parts);
|
|
4719
5286
|
|
|
4720
5287
|
const url = `${getConfluenceBaseUrl(inst)}/rest/api/content/${encodeURIComponent(args.pageId)}/child/attachment`;
|
|
5288
|
+
if (_policyCtx.dryRun) {
|
|
5289
|
+
recordDryRunCall({
|
|
5290
|
+
api: "confluence",
|
|
5291
|
+
method: "POST",
|
|
5292
|
+
endpoint: `/rest/api/content/${args.pageId}/child/attachment`,
|
|
5293
|
+
body: `<multipart upload: ${fileName}, ${fileBuffer.length} bytes>`,
|
|
5294
|
+
instance: inst.name,
|
|
5295
|
+
});
|
|
5296
|
+
}
|
|
4721
5297
|
const response = await fetch(url, {
|
|
4722
5298
|
method: "POST",
|
|
4723
5299
|
headers: {
|
|
@@ -4755,12 +5331,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4755
5331
|
} else {
|
|
4756
5332
|
throw new Error(`Unknown tool: ${name}`);
|
|
4757
5333
|
}
|
|
4758
|
-
}
|
|
4759
|
-
return {
|
|
4760
|
-
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
4761
|
-
isError: true,
|
|
4762
|
-
};
|
|
4763
|
-
}
|
|
5334
|
+
});
|
|
4764
5335
|
});
|
|
4765
5336
|
|
|
4766
5337
|
async function main() {
|
|
@@ -4774,5 +5345,14 @@ if (require.main === module) {
|
|
|
4774
5345
|
|
|
4775
5346
|
// Export for testing
|
|
4776
5347
|
if (typeof module !== "undefined") {
|
|
4777
|
-
module.exports = {
|
|
5348
|
+
module.exports = {
|
|
5349
|
+
buildCommentADF, parseInlineFormatting, autoLinkTextNodes, findJiraTicketKeys,
|
|
5350
|
+
resolveTeamId, fetchJiraTeams, listTeams, searchTeamsViaJql,
|
|
5351
|
+
fetchConfluence, getConfluenceBaseUrl, textToConfluenceStorage, htmlEscape,
|
|
5352
|
+
// Policy module exports
|
|
5353
|
+
TOOL_METADATA, checkScope, resolveDryRun, scrubArgs, extractTarget,
|
|
5354
|
+
checkRateLimit, _setClockForTests, _resetBucketsForTests,
|
|
5355
|
+
resolveInstanceForTool, _injectDryRunSchema,
|
|
5356
|
+
AUDIT_LOG_PATH,
|
|
5357
|
+
};
|
|
4778
5358
|
}
|