@jam-mcp/server 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/adapters/jira-cloud/jira-read.adapter.js +1 -11
- package/dist/application/apply-write.js +5 -52
- package/dist/application/plan-write.d.ts +2 -15
- package/dist/application/plan-write.js +6 -108
- package/dist/bootstrap/mcp-config-merger.d.ts +1 -1
- package/dist/config/schema.d.ts +0 -1
- package/dist/config/schema.js +1 -35
- package/dist/deps.d.ts +0 -9
- package/dist/deps.js +0 -6
- package/dist/domain/errors.d.ts +1 -1
- package/dist/domain/errors.js +0 -6
- package/dist/domain/write.d.ts +3 -99
- package/dist/domain/write.js +0 -11
- package/dist/mcp/tools/jira-write-plan.tool.js +0 -12
- package/dist/ports/jira-read.port.d.ts +0 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -74,10 +74,10 @@ auth login Store Jira credentials in this user's OS secret store
|
|
|
74
74
|
runtime Show or change which JAM build this machine runs
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Written out, that is `npx --yes @jam-mcp/launcher@1.3.
|
|
77
|
+
Written out, that is `npx --yes @jam-mcp/launcher@1.3.2 doctor`, or just `jam
|
|
78
78
|
doctor` if you took the launcher's optional global install. Starting from
|
|
79
79
|
nothing — no install, no runtime chosen yet — use
|
|
80
|
-
`npx --yes @jam-mcp/bootstrap@1.3.
|
|
80
|
+
`npx --yes @jam-mcp/bootstrap@1.3.2 init` instead.
|
|
81
81
|
|
|
82
82
|
Credentials come from the process environment or this user's OS secret store —
|
|
83
83
|
never from a repository file — and never appear in logs, telemetry, or tool
|
|
@@ -45,21 +45,11 @@ export class JiraCloudReadAdapter {
|
|
|
45
45
|
// Read straight off the raw payload rather than through the mapper: the
|
|
46
46
|
// mapper's job is the shape the read tools see, and this identity is only
|
|
47
47
|
// for the write plane. Raw DTOs still stop here.
|
|
48
|
-
const
|
|
49
|
-
const assignee = raw?.["assignee"];
|
|
48
|
+
const assignee = data.fields?.assignee;
|
|
50
49
|
const accountId = typeof assignee?.accountId === "string" ? assignee.accountId : undefined;
|
|
51
|
-
// Only the ids that were asked for, and only when some were: a caller that
|
|
52
|
-
// did not request a custom field gets no entry rather than an empty object
|
|
53
|
-
// it has to tell apart from a field that is genuinely unset.
|
|
54
|
-
const customFieldValues = {};
|
|
55
|
-
for (const field of req.fields) {
|
|
56
|
-
if (field.startsWith("customfield_"))
|
|
57
|
-
customFieldValues[field] = raw?.[field] ?? null;
|
|
58
|
-
}
|
|
59
50
|
return {
|
|
60
51
|
issue: mapIssueWithMeta(data, this.config).issue,
|
|
61
52
|
...(accountId ? { assigneeAccountId: accountId } : {}),
|
|
62
|
-
...(Object.keys(customFieldValues).length > 0 ? { customFieldValues } : {}),
|
|
63
53
|
responseBytes: bytes,
|
|
64
54
|
};
|
|
65
55
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { JamError, toJamError } from "../domain/errors.js";
|
|
2
2
|
import { readModeAfterWrite } from "../policy/consistency-policy.js";
|
|
3
3
|
import { assertAssignable } from "../policy/assignee-policy.js";
|
|
4
|
-
import { assertCustomFieldUnchanged } from "../policy/custom-field-policy.js";
|
|
5
4
|
import { assertUnchanged } from "../policy/write-policy.js";
|
|
6
5
|
import { applyCreateIssue } from "./apply-create-issue.js";
|
|
7
|
-
import {
|
|
6
|
+
import { readIssue } from "./plan-write.js";
|
|
8
7
|
/**
|
|
9
8
|
* Execute a plan JAM made, then go and look at what happened.
|
|
10
9
|
*
|
|
@@ -63,18 +62,10 @@ export async function applyWritePlan(deps, request) {
|
|
|
63
62
|
* own state, which `assertUnchanged` already compared.
|
|
64
63
|
*/
|
|
65
64
|
async function revalidate(deps, plan) {
|
|
66
|
-
if (plan.mutation.kind
|
|
67
|
-
const target = plan.intendedAfter["assignee"];
|
|
68
|
-
assertAssignable(plan.issueKey, target, await deps.jiraAssignees.isAssignable(plan.issueKey, plan.mutation.accountId));
|
|
65
|
+
if (plan.mutation.kind !== "assignee")
|
|
69
66
|
return;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// A field can be taken off a screen, lose its `set` operation, change type
|
|
73
|
-
// or have an option renamed without the issue's own revision moving, so
|
|
74
|
-
// `assertUnchanged` cannot see any of it. These are the premises the plan
|
|
75
|
-
// actually rested on, re-derived.
|
|
76
|
-
assertCustomFieldUnchanged(plan.issueKey, plan.customFieldRequirements, await deps.jiraEditMetadata.getEditableFields(plan.issueKey));
|
|
77
|
-
}
|
|
67
|
+
const target = plan.intendedAfter["assignee"];
|
|
68
|
+
assertAssignable(plan.issueKey, target, await deps.jiraAssignees.isAssignable(plan.issueKey, plan.mutation.accountId));
|
|
78
69
|
}
|
|
79
70
|
/**
|
|
80
71
|
* Send the mutation, once.
|
|
@@ -101,14 +92,6 @@ async function mutate(deps, plan) {
|
|
|
101
92
|
case "assignee":
|
|
102
93
|
await deps.jiraWrite.assignIssue(plan.issueKey, plan.mutation.accountId);
|
|
103
94
|
return {};
|
|
104
|
-
case "custom-field":
|
|
105
|
-
// The ordinary issue edit endpoint. A custom field is a field; what
|
|
106
|
-
// made it need its own operation was deciding whether it may be
|
|
107
|
-
// written and in what shape, and that is already settled here.
|
|
108
|
-
await deps.jiraWrite.updateIssue(plan.issueKey, {
|
|
109
|
-
[plan.mutation.fieldId]: plan.mutation.value,
|
|
110
|
-
});
|
|
111
|
-
return {};
|
|
112
95
|
case "create":
|
|
113
96
|
// Unreachable: a create plan is routed to applyCreateIssue above. The
|
|
114
97
|
// case exists so adding a mutation kind is a compile error here rather
|
|
@@ -141,7 +124,7 @@ function isAmbiguous(err) {
|
|
|
141
124
|
* ours.
|
|
142
125
|
*/
|
|
143
126
|
async function verify(deps, plan) {
|
|
144
|
-
const snapshot = await readIssue(deps, plan.issueKey
|
|
127
|
+
const snapshot = await readIssue(deps, plan.issueKey);
|
|
145
128
|
const issue = snapshot.issue;
|
|
146
129
|
if (plan.mutation.kind === "assignee") {
|
|
147
130
|
// On the accountId, never on the display name. Two people can share a
|
|
@@ -173,15 +156,6 @@ async function verify(deps, plan) {
|
|
|
173
156
|
}
|
|
174
157
|
return { comments: comments.length, commentAdded: wanted };
|
|
175
158
|
}
|
|
176
|
-
if (plan.mutation.kind === "custom-field" && plan.customFieldRequirements) {
|
|
177
|
-
const requirements = plan.customFieldRequirements;
|
|
178
|
-
const expected = plan.intendedAfter["customField"];
|
|
179
|
-
const observedValue = currentCustomFieldView({ id: requirements.fieldId, name: requirements.fieldName }, requirements.kind, snapshot.customFieldValues?.[requirements.fieldId]);
|
|
180
|
-
if (!sameCustomFieldValue(requirements.kind, expected.value, observedValue.value)) {
|
|
181
|
-
throw verificationFailed(plan, { customField: expected }, { customField: observedValue });
|
|
182
|
-
}
|
|
183
|
-
return { customField: observedValue };
|
|
184
|
-
}
|
|
185
159
|
const observed = observedFor(plan, issue);
|
|
186
160
|
for (const [field, expected] of Object.entries(plan.intendedAfter)) {
|
|
187
161
|
if (!sameValue(observed[field], expected)) {
|
|
@@ -215,27 +189,6 @@ function observedFor(plan, issue) {
|
|
|
215
189
|
}
|
|
216
190
|
return observed;
|
|
217
191
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Did the field end up holding what was planned?
|
|
220
|
-
*
|
|
221
|
-
* Options are compared on their ids, never on their labels - an option is
|
|
222
|
-
* identified by its id, and a label is what a person reads. For a multi-select
|
|
223
|
-
* the comparison is set-wise: Jira is free to return the same selection in a
|
|
224
|
-
* different order, and that is not a different selection.
|
|
225
|
-
*/
|
|
226
|
-
function sameCustomFieldValue(kind, expected, observed) {
|
|
227
|
-
if (kind === "multi-option") {
|
|
228
|
-
const ids = (v) => (Array.isArray(v) ? v.map((o) => o.id) : []).sort();
|
|
229
|
-
const a = ids(expected);
|
|
230
|
-
const b = ids(observed);
|
|
231
|
-
return a.length === b.length && a.every((id, i) => id === b[i]);
|
|
232
|
-
}
|
|
233
|
-
if (kind === "single-option") {
|
|
234
|
-
const id = (v) => v && typeof v === "object" && !Array.isArray(v) ? v.id : undefined;
|
|
235
|
-
return id(expected) === id(observed);
|
|
236
|
-
}
|
|
237
|
-
return expected === observed;
|
|
238
|
-
}
|
|
239
192
|
function sameValue(observed, expected) {
|
|
240
193
|
if (Array.isArray(expected) || Array.isArray(observed)) {
|
|
241
194
|
const a = Array.isArray(observed) ? [...observed].map(String).sort() : [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JamDeps } from "../deps.js";
|
|
2
2
|
import type { FullIssueContext } from "../domain/context.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { WritePlan, WritePlanReceipt } from "../domain/write.js";
|
|
4
4
|
export type PlanWriteRequest = {
|
|
5
5
|
/** Absent for `issue.create`, which names a project rather than an issue. */
|
|
6
6
|
key?: string;
|
|
@@ -40,18 +40,5 @@ export type IssueSnapshot = {
|
|
|
40
40
|
issue: FullIssueContext;
|
|
41
41
|
/** Identity of the current assignee, which `issue.assignee` cannot supply. */
|
|
42
42
|
assigneeAccountId?: string;
|
|
43
|
-
/** Raw values for any custom field ids that were asked for. */
|
|
44
|
-
customFieldValues?: Record<string, unknown>;
|
|
45
43
|
};
|
|
46
|
-
export declare function readIssue(deps: JamDeps, issueKey: string
|
|
47
|
-
/**
|
|
48
|
-
* What the field holds now, in the shape a receipt shows.
|
|
49
|
-
*
|
|
50
|
-
* Jira stores an option as an object and a scalar as itself; a person reading
|
|
51
|
-
* `before` wants the same canonical form they will see in `intendedAfter`, so
|
|
52
|
-
* they can compare the two rather than a payload against a summary.
|
|
53
|
-
*/
|
|
54
|
-
export declare function currentCustomFieldView(field: {
|
|
55
|
-
id: string;
|
|
56
|
-
name: string;
|
|
57
|
-
}, kind: CustomFieldKind, raw: unknown): CustomFieldValueView;
|
|
44
|
+
export declare function readIssue(deps: JamDeps, issueKey: string): Promise<IssueSnapshot>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { JamError } from "../domain/errors.js";
|
|
2
2
|
import { assertExistingIssueOperation, assertFieldsAllowed, assertOperationAllowed, assertWriteScope, PLAN_TTL_MS, resolveTransition, } from "../policy/write-policy.js";
|
|
3
3
|
import { assertAssignable, assertNotAlreadyAssigned, exactMatches, resolveAssignee, } from "../policy/assignee-policy.js";
|
|
4
|
-
import { assertEditable, classifyKind, resolveCustomFieldValue, resolveWritableField, } from "../policy/custom-field-policy.js";
|
|
5
4
|
import { planCreateIssue } from "./plan-create-issue.js";
|
|
6
5
|
/**
|
|
7
6
|
* Work out whether a requested change is currently possible, and describe it.
|
|
@@ -32,15 +31,9 @@ export async function planWrite(deps, request) {
|
|
|
32
31
|
// does not write should get that answer, not a round trip and then that
|
|
33
32
|
// answer.
|
|
34
33
|
const input = validateInput(operation, request.input);
|
|
35
|
-
|
|
36
|
-
// the read can fetch its current value in the same request. A selector the
|
|
37
|
-
// team never opted in costs no Jira call at all.
|
|
38
|
-
const targetField = operation === "custom-field.update"
|
|
39
|
-
? resolveWritableField(deps.config, input.field)
|
|
40
|
-
: undefined;
|
|
41
|
-
const snapshot = await readIssue(deps, issueKey, targetField ? [targetField.id] : []);
|
|
34
|
+
const snapshot = await readIssue(deps, issueKey);
|
|
42
35
|
const issue = snapshot.issue;
|
|
43
|
-
const { before, intendedAfter, mutation, transition, baseAssigneeAccountId
|
|
36
|
+
const { before, intendedAfter, mutation, transition, baseAssigneeAccountId } = await describe(deps, operation, issueKey, snapshot, input);
|
|
44
37
|
const createdAt = new Date();
|
|
45
38
|
const plan = deps.writePlans.create({
|
|
46
39
|
kind: "existing-issue",
|
|
@@ -54,7 +47,6 @@ export async function planWrite(deps, request) {
|
|
|
54
47
|
expiresAt: new Date(createdAt.getTime() + PLAN_TTL_MS).toISOString(),
|
|
55
48
|
...(transition ? { transition } : {}),
|
|
56
49
|
...(baseAssigneeAccountId ? { baseAssigneeAccountId } : {}),
|
|
57
|
-
...(customFieldRequirements ? { customFieldRequirements } : {}),
|
|
58
50
|
mutation,
|
|
59
51
|
});
|
|
60
52
|
return {
|
|
@@ -71,8 +63,8 @@ export async function planWrite(deps, request) {
|
|
|
71
63
|
},
|
|
72
64
|
};
|
|
73
65
|
}
|
|
74
|
-
export async function readIssue(deps, issueKey
|
|
75
|
-
const { issue: found, assigneeAccountId
|
|
66
|
+
export async function readIssue(deps, issueKey) {
|
|
67
|
+
const { issue: found, assigneeAccountId } = await deps.jira.getIssue({
|
|
76
68
|
key: issueKey,
|
|
77
69
|
// `issuetype` and `description` are here for creation's verification step,
|
|
78
70
|
// which has to confirm the issue Jira made is the one that was asked for.
|
|
@@ -89,33 +81,13 @@ export async function readIssue(deps, issueKey, extraFields = []) {
|
|
|
89
81
|
"labels",
|
|
90
82
|
"components",
|
|
91
83
|
"updated",
|
|
92
|
-
// A custom field is only read when one is being written, and then only
|
|
93
|
-
// that one - so a custom-field update still costs a single direct GET
|
|
94
|
-
// rather than a second read for the field it is about to change.
|
|
95
|
-
...extraFields.filter((f) => !BASE_WRITE_FIELDS.has(f)),
|
|
96
84
|
],
|
|
97
85
|
});
|
|
98
86
|
if (!found) {
|
|
99
87
|
throw new JamError("ISSUE_NOT_FOUND", `Jira has no issue ${issueKey}, or it is not visible to this account.`, { issueKey });
|
|
100
88
|
}
|
|
101
|
-
return {
|
|
102
|
-
issue: found,
|
|
103
|
-
...(assigneeAccountId ? { assigneeAccountId } : {}),
|
|
104
|
-
...(customFieldValues ? { customFieldValues } : {}),
|
|
105
|
-
};
|
|
89
|
+
return { issue: found, ...(assigneeAccountId ? { assigneeAccountId } : {}) };
|
|
106
90
|
}
|
|
107
|
-
/** Requested on every write-plane read, so an extra field is never a duplicate. */
|
|
108
|
-
const BASE_WRITE_FIELDS = new Set([
|
|
109
|
-
"summary",
|
|
110
|
-
"status",
|
|
111
|
-
"issuetype",
|
|
112
|
-
"description",
|
|
113
|
-
"assignee",
|
|
114
|
-
"priority",
|
|
115
|
-
"labels",
|
|
116
|
-
"components",
|
|
117
|
-
"updated",
|
|
118
|
-
]);
|
|
119
91
|
/**
|
|
120
92
|
* The issue an existing-issue operation names, or a refusal that says why.
|
|
121
93
|
*
|
|
@@ -156,30 +128,6 @@ function validateInput(operation, raw) {
|
|
|
156
128
|
}
|
|
157
129
|
return { status: status.trim() };
|
|
158
130
|
}
|
|
159
|
-
case "custom-field.update": {
|
|
160
|
-
// Anything outside this operation's own two keys is refused rather than
|
|
161
|
-
// ignored. Silently dropping a key an agent supplied is how a caller
|
|
162
|
-
// ends up with a write that is not the one it described - and the shared
|
|
163
|
-
// input object means another operation's key is a plausible mistake.
|
|
164
|
-
const extra = Object.keys(raw).filter((k) => raw[k] !== undefined && k !== "field" && k !== "value");
|
|
165
|
-
if (extra.length > 0) {
|
|
166
|
-
throw new JamError("JAM_WRITE_FIELD_NOT_ALLOWED", `custom-field.update takes only \`field\` and \`value\`. Remove: ${extra.join(", ")}.`, { operation, rejected: extra });
|
|
167
|
-
}
|
|
168
|
-
const { field, value } = raw;
|
|
169
|
-
if (typeof field !== "string" || field.trim().length === 0) {
|
|
170
|
-
throw new JamError("JAM_WRITE_OPERATION_NOT_ALLOWED", "custom-field.update needs non-empty `input.field` - a configured custom field id or name.", { operation });
|
|
171
|
-
}
|
|
172
|
-
// The value's family is checked against Jira's schema later; what is
|
|
173
|
-
// checked here is that it is a shape the contract admits at all. An
|
|
174
|
-
// object, a boolean or a null never reaches the type policy.
|
|
175
|
-
const isString = typeof value === "string";
|
|
176
|
-
const isNumber = typeof value === "number";
|
|
177
|
-
const isStringArray = Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
178
|
-
if (!isString && !isNumber && !isStringArray) {
|
|
179
|
-
throw new JamError("JAM_WRITE_OPERATION_NOT_ALLOWED", "custom-field.update needs `input.value` to be a string, a number, or an array of strings.", { operation, received: Array.isArray(value) ? "array" : typeof value });
|
|
180
|
-
}
|
|
181
|
-
return { field: field.trim(), value };
|
|
182
|
-
}
|
|
183
131
|
case "assignee.update": {
|
|
184
132
|
const assignee = raw.assignee;
|
|
185
133
|
if (typeof assignee !== "string" || assignee.trim().length === 0) {
|
|
@@ -189,7 +137,7 @@ function validateInput(operation, raw) {
|
|
|
189
137
|
}
|
|
190
138
|
}
|
|
191
139
|
}
|
|
192
|
-
async function describe(deps, operation, issueKey, snapshot, input
|
|
140
|
+
async function describe(deps, operation, issueKey, snapshot, input) {
|
|
193
141
|
const issue = snapshot.issue;
|
|
194
142
|
switch (operation) {
|
|
195
143
|
case "comment.add": {
|
|
@@ -230,31 +178,6 @@ async function describe(deps, operation, issueKey, snapshot, input, targetField)
|
|
|
230
178
|
transition,
|
|
231
179
|
};
|
|
232
180
|
}
|
|
233
|
-
case "custom-field.update": {
|
|
234
|
-
const field = targetField;
|
|
235
|
-
const requested = input;
|
|
236
|
-
// Jira decides what is editable here and now, and in what shape. JAM
|
|
237
|
-
// does not model project contexts, screens or permissions - it asks the
|
|
238
|
-
// one endpoint that answers all three at once for this issue.
|
|
239
|
-
const metadata = await deps.jiraEditMetadata.getEditableFields(issueKey);
|
|
240
|
-
const editable = assertEditable(issueKey, field, metadata);
|
|
241
|
-
const kind = classifyKind(editable);
|
|
242
|
-
const { jiraValue, view, resolvedOptions } = resolveCustomFieldValue(editable, kind, requested);
|
|
243
|
-
return {
|
|
244
|
-
before: {
|
|
245
|
-
customField: currentCustomFieldView(field, kind, snapshot.customFieldValues?.[field.id]),
|
|
246
|
-
},
|
|
247
|
-
intendedAfter: { customField: view },
|
|
248
|
-
mutation: { kind: "custom-field", fieldId: field.id, value: jiraValue },
|
|
249
|
-
customFieldRequirements: {
|
|
250
|
-
fieldId: field.id,
|
|
251
|
-
fieldName: field.name,
|
|
252
|
-
kind,
|
|
253
|
-
schema: editable.schema,
|
|
254
|
-
...(resolvedOptions ? { resolvedOptions } : {}),
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
181
|
case "assignee.update": {
|
|
259
182
|
const { assignee: requested } = input;
|
|
260
183
|
// Ask Jira who this is, and decide from what it says. The requested
|
|
@@ -317,31 +240,6 @@ function currentValue(issue, field) {
|
|
|
317
240
|
return undefined;
|
|
318
241
|
}
|
|
319
242
|
}
|
|
320
|
-
/**
|
|
321
|
-
* What the field holds now, in the shape a receipt shows.
|
|
322
|
-
*
|
|
323
|
-
* Jira stores an option as an object and a scalar as itself; a person reading
|
|
324
|
-
* `before` wants the same canonical form they will see in `intendedAfter`, so
|
|
325
|
-
* they can compare the two rather than a payload against a summary.
|
|
326
|
-
*/
|
|
327
|
-
export function currentCustomFieldView(field, kind, raw) {
|
|
328
|
-
const named = { id: field.id, name: field.name };
|
|
329
|
-
if (kind === "multi-option") {
|
|
330
|
-
return { ...named, value: Array.isArray(raw) ? raw.map(toOptionView) : [] };
|
|
331
|
-
}
|
|
332
|
-
if (kind === "single-option") {
|
|
333
|
-
return { ...named, value: raw == null ? null : toOptionView(raw) };
|
|
334
|
-
}
|
|
335
|
-
if (raw == null)
|
|
336
|
-
return { ...named, value: null };
|
|
337
|
-
return { ...named, value: kind === "number" ? Number(raw) : String(raw) };
|
|
338
|
-
}
|
|
339
|
-
function toOptionView(raw) {
|
|
340
|
-
const o = raw;
|
|
341
|
-
const id = typeof o?.id === "string" ? o.id : typeof o?.id === "number" ? String(o.id) : "";
|
|
342
|
-
const label = typeof o?.value === "string" ? o.value : typeof o?.name === "string" ? o.name : String(raw);
|
|
343
|
-
return { id, label };
|
|
344
|
-
}
|
|
345
243
|
/** Whitelisted values to the shapes Jira's field API expects. */
|
|
346
244
|
function toJiraFields(input) {
|
|
347
245
|
const fields = {};
|
|
@@ -13,7 +13,7 @@ import { LAUNCHER_PACKAGE_SPEC } from "@jam-mcp/launcher";
|
|
|
13
13
|
export { LAUNCHER_PACKAGE_SPEC };
|
|
14
14
|
export declare const JAM_MCP_ENTRY: {
|
|
15
15
|
readonly command: "npx";
|
|
16
|
-
readonly args: readonly ["--yes", "@jam-mcp/launcher@1.3.
|
|
16
|
+
readonly args: readonly ["--yes", "@jam-mcp/launcher@1.3.2", "serve"];
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
19
|
* Recognise wiring from before the launcher existed: a hard-coded path to one
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -19,7 +19,6 @@ export declare const ProjectConfigSchema: z.ZodObject<{
|
|
|
19
19
|
customFields: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
20
20
|
id: z.ZodString;
|
|
21
21
|
name: z.ZodString;
|
|
22
|
-
writable: z.ZodDefault<z.ZodBoolean>;
|
|
23
22
|
}, z.core.$strip>>>;
|
|
24
23
|
output: z.ZodPrefault<z.ZodObject<{
|
|
25
24
|
searchTokens: z.ZodDefault<z.ZodNumber>;
|
package/dist/config/schema.js
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
/** Values that appear more than once, each named once. */
|
|
3
|
-
function duplicates(values) {
|
|
4
|
-
const seen = new Set();
|
|
5
|
-
const repeated = new Set();
|
|
6
|
-
for (const value of values) {
|
|
7
|
-
if (seen.has(value))
|
|
8
|
-
repeated.add(value);
|
|
9
|
-
seen.add(value);
|
|
10
|
-
}
|
|
11
|
-
return [...repeated];
|
|
12
|
-
}
|
|
13
|
-
function report(ctx, repeated, what) {
|
|
14
|
-
for (const value of repeated) {
|
|
15
|
-
ctx.addIssue({
|
|
16
|
-
code: "custom",
|
|
17
|
-
message: `duplicate custom field ${what} "${value}" - a selector must name one field`,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
2
|
/**
|
|
22
3
|
* `.jira-agent/project.yaml` - per-project policy only.
|
|
23
4
|
* Credentials are never stored here; they come from the CredentialPort.
|
|
@@ -60,28 +41,13 @@ export const ProjectConfigSchema = z.object({
|
|
|
60
41
|
/**
|
|
61
42
|
* Whitelisted project-specific custom fields, surfaced at CONTEXT level and up.
|
|
62
43
|
* `id` is the Jira field id (customfield_10011); `name` is what the agent sees.
|
|
63
|
-
*
|
|
64
|
-
* `writable` is a second, separate consent. Reading a field and letting an
|
|
65
|
-
* agent change it are different decisions, and a config written when JAM
|
|
66
|
-
* could only read must not start granting writes because JAM learned how.
|
|
67
|
-
* So it defaults to false: every whitelist that predates this is read-only,
|
|
68
|
-
* and a team opts a field in by saying so.
|
|
69
44
|
*/
|
|
70
45
|
customFields: z
|
|
71
46
|
.array(z.object({
|
|
72
47
|
id: z.string().regex(/^customfield_\d+$/),
|
|
73
48
|
name: z.string().min(1),
|
|
74
|
-
writable: z.boolean().default(false),
|
|
75
49
|
}))
|
|
76
|
-
.default([])
|
|
77
|
-
.superRefine((fields, ctx) => {
|
|
78
|
-
// Ambiguity in a whitelist is worse than an omission: `custom-field.update`
|
|
79
|
-
// resolves a selector against these entries, and two rows answering to
|
|
80
|
-
// the same selector would make which field gets written a matter of
|
|
81
|
-
// ordering.
|
|
82
|
-
report(ctx, duplicates(fields.map((f) => f.id.toLowerCase())), "id");
|
|
83
|
-
report(ctx, duplicates(fields.filter((f) => f.writable).map((f) => f.name.trim().toLowerCase())), "writable name");
|
|
84
|
-
}),
|
|
50
|
+
.default([]),
|
|
85
51
|
output: z
|
|
86
52
|
.object({
|
|
87
53
|
/** Rough token ceilings per level. Enforced by OutputBudgetPolicy. */
|
package/dist/deps.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { CredentialPort } from "./ports/credentials.port.js";
|
|
|
6
6
|
import type { JiraReadPort } from "./ports/jira-read.port.js";
|
|
7
7
|
import type { JiraAssigneeResolutionPort } from "./ports/jira-assignee-resolution.port.js";
|
|
8
8
|
import type { JiraCreateMetadataPort } from "./ports/jira-create-metadata.port.js";
|
|
9
|
-
import type { JiraEditMetadataPort } from "./ports/jira-edit-metadata.port.js";
|
|
10
9
|
import type { JiraWritePort } from "./ports/jira-write.port.js";
|
|
11
10
|
import { WritePlanStore } from "./application/write-plan-store.js";
|
|
12
11
|
import type { TelemetryPort } from "./ports/telemetry.port.js";
|
|
@@ -37,12 +36,6 @@ export type JamDeps = {
|
|
|
37
36
|
* it mutates nothing.
|
|
38
37
|
*/
|
|
39
38
|
jiraAssignees: JiraAssigneeResolutionPort;
|
|
40
|
-
/**
|
|
41
|
-
* What Jira will let this account change on one issue. A fifth read-shaped
|
|
42
|
-
* port, for the same reason as the third and fourth: it mutates nothing, and
|
|
43
|
-
* it answers a question about a configuration rather than about an issue.
|
|
44
|
-
*/
|
|
45
|
-
jiraEditMetadata: JiraEditMetadataPort;
|
|
46
39
|
/**
|
|
47
40
|
* Plans awaiting apply. Lives for the life of this server process - see
|
|
48
41
|
* WritePlanStore for why it is not persisted.
|
|
@@ -62,8 +55,6 @@ export type BuildDepsOptions = {
|
|
|
62
55
|
jiraCreateMetadata?: JiraCreateMetadataPort;
|
|
63
56
|
/** Injected by tests so user resolution never reaches a real directory. */
|
|
64
57
|
jiraAssignees?: JiraAssigneeResolutionPort;
|
|
65
|
-
/** Injected by tests so edit metadata comes from a fixture, not a site. */
|
|
66
|
-
jiraEditMetadata?: JiraEditMetadataPort;
|
|
67
58
|
/** Injected by tests to bypass the real process/registry credential lookup. */
|
|
68
59
|
credentials?: CredentialPort;
|
|
69
60
|
/**
|
package/dist/deps.js
CHANGED
|
@@ -43,11 +43,6 @@ export async function buildDeps(options = {}) {
|
|
|
43
43
|
const { JiraCloudAssigneeResolutionAdapter } = await import("./adapters/jira-cloud/jira-assignee-resolution.adapter.js");
|
|
44
44
|
jiraAssignees = new JiraCloudAssigneeResolutionAdapter(credentials);
|
|
45
45
|
}
|
|
46
|
-
let jiraEditMetadata = options.jiraEditMetadata;
|
|
47
|
-
if (!jiraEditMetadata) {
|
|
48
|
-
const { JiraCloudEditMetadataAdapter } = await import("./adapters/jira-cloud/jira-edit-metadata.adapter.js");
|
|
49
|
-
jiraEditMetadata = new JiraCloudEditMetadataAdapter(credentials);
|
|
50
|
-
}
|
|
51
46
|
return {
|
|
52
47
|
config: resolved.config,
|
|
53
48
|
configPath: resolved.configPath,
|
|
@@ -56,7 +51,6 @@ export async function buildDeps(options = {}) {
|
|
|
56
51
|
jiraWrite,
|
|
57
52
|
jiraCreateMetadata,
|
|
58
53
|
jiraAssignees,
|
|
59
|
-
jiraEditMetadata,
|
|
60
54
|
writePlans: new WritePlanStore(),
|
|
61
55
|
cache: new NoopCache(),
|
|
62
56
|
telemetry,
|
package/dist/domain/errors.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* is mapped onto one of these codes so the agent (and `jam doctor`) can reason
|
|
6
6
|
* about failures without parsing vendor-specific payloads.
|
|
7
7
|
*/
|
|
8
|
-
export declare const JAM_ERROR_CODES: readonly ["JIRA_AUTH_FAILED", "JIRA_PERMISSION_DENIED", "JQL_INVALID", "ISSUE_NOT_FOUND", "RATE_LIMITED", "CONTEXT_TOO_LARGE", "PARTIAL_RESULT", "CONFIG_INVALID", "JIRA_UNAVAILABLE", "JAM_SETUP_REQUIRED", "JAM_BINDINGS_UNREADABLE", "JAM_WRITE_SCOPE_VIOLATION", "JAM_WRITE_OPERATION_NOT_ALLOWED", "JAM_WRITE_FIELD_NOT_ALLOWED", "JAM_WRITE_TRANSITION_NOT_AVAILABLE", "JAM_WRITE_ISSUE_TYPE_NOT_AVAILABLE", "JAM_WRITE_REQUIRED_FIELD_UNSUPPORTED", "JAM_WRITE_VALUE_NOT_ALLOWED", "JAM_WRITE_SCHEMA_CHANGED", "JAM_WRITE_ASSIGNEE_NOT_FOUND", "JAM_WRITE_ASSIGNEE_AMBIGUOUS", "JAM_WRITE_ASSIGNEE_NOT_ASSIGNABLE", "JAM_WRITE_ASSIGNEE_ALREADY_SET", "
|
|
8
|
+
export declare const JAM_ERROR_CODES: readonly ["JIRA_AUTH_FAILED", "JIRA_PERMISSION_DENIED", "JQL_INVALID", "ISSUE_NOT_FOUND", "RATE_LIMITED", "CONTEXT_TOO_LARGE", "PARTIAL_RESULT", "CONFIG_INVALID", "JIRA_UNAVAILABLE", "JAM_SETUP_REQUIRED", "JAM_BINDINGS_UNREADABLE", "JAM_WRITE_SCOPE_VIOLATION", "JAM_WRITE_OPERATION_NOT_ALLOWED", "JAM_WRITE_FIELD_NOT_ALLOWED", "JAM_WRITE_TRANSITION_NOT_AVAILABLE", "JAM_WRITE_ISSUE_TYPE_NOT_AVAILABLE", "JAM_WRITE_REQUIRED_FIELD_UNSUPPORTED", "JAM_WRITE_VALUE_NOT_ALLOWED", "JAM_WRITE_SCHEMA_CHANGED", "JAM_WRITE_ASSIGNEE_NOT_FOUND", "JAM_WRITE_ASSIGNEE_AMBIGUOUS", "JAM_WRITE_ASSIGNEE_NOT_ASSIGNABLE", "JAM_WRITE_ASSIGNEE_ALREADY_SET", "JAM_WRITE_PLAN_NOT_FOUND", "JAM_WRITE_PLAN_EXPIRED", "JAM_WRITE_CONFLICT", "JAM_WRITE_VERIFICATION_FAILED", "JAM_WRITE_UNCERTAIN"];
|
|
9
9
|
export type JamErrorCode = (typeof JAM_ERROR_CODES)[number];
|
|
10
10
|
export type JamErrorPayload = {
|
|
11
11
|
error: {
|
package/dist/domain/errors.js
CHANGED
|
@@ -42,12 +42,6 @@ export const JAM_ERROR_CODES = [
|
|
|
42
42
|
"JAM_WRITE_ASSIGNEE_AMBIGUOUS",
|
|
43
43
|
"JAM_WRITE_ASSIGNEE_NOT_ASSIGNABLE",
|
|
44
44
|
"JAM_WRITE_ASSIGNEE_ALREADY_SET",
|
|
45
|
-
// Custom fields. Three permissions have to line up and none implies another,
|
|
46
|
-
// so a refusal says which one is missing: the team never opted this field in,
|
|
47
|
-
// Jira will not let it be set on this issue, or JAM does not know the type
|
|
48
|
-
// well enough to write it. Each points somewhere different.
|
|
49
|
-
"JAM_WRITE_CUSTOM_FIELD_NOT_EDITABLE",
|
|
50
|
-
"JAM_WRITE_CUSTOM_FIELD_TYPE_UNSUPPORTED",
|
|
51
45
|
"JAM_WRITE_PLAN_NOT_FOUND",
|
|
52
46
|
"JAM_WRITE_PLAN_EXPIRED",
|
|
53
47
|
"JAM_WRITE_CONFLICT",
|
package/dist/domain/write.d.ts
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* layer: these name an issue, creation names a project; these compare a
|
|
21
21
|
* revision to detect a conflict, creation has no revision to compare.
|
|
22
22
|
*/
|
|
23
|
-
export declare const EXISTING_ISSUE_OPERATIONS: readonly ["comment.add", "field.update", "status.transition", "assignee.update"
|
|
23
|
+
export declare const EXISTING_ISSUE_OPERATIONS: readonly ["comment.add", "field.update", "status.transition", "assignee.update"];
|
|
24
24
|
/** The operations the public MCP surface accepts. Nothing else is reachable. */
|
|
25
|
-
export declare const WRITE_OPERATIONS: readonly ["comment.add", "field.update", "status.transition", "assignee.update", "
|
|
25
|
+
export declare const WRITE_OPERATIONS: readonly ["comment.add", "field.update", "status.transition", "assignee.update", "issue.create"];
|
|
26
26
|
export type ExistingIssueOperation = (typeof EXISTING_ISSUE_OPERATIONS)[number];
|
|
27
27
|
export type WriteOperation = (typeof WRITE_OPERATIONS)[number];
|
|
28
28
|
/**
|
|
@@ -93,7 +93,7 @@ export type CreateIssueInput = {
|
|
|
93
93
|
labels?: string[];
|
|
94
94
|
components?: string[];
|
|
95
95
|
};
|
|
96
|
-
export type WriteInput = CommentAddInput | FieldUpdateInput | StatusTransitionInput | AssigneeUpdateInput |
|
|
96
|
+
export type WriteInput = CommentAddInput | FieldUpdateInput | StatusTransitionInput | AssigneeUpdateInput | CreateIssueInput;
|
|
97
97
|
/** An issue type as Jira offers it for one project, right now. */
|
|
98
98
|
export type CreateIssueType = {
|
|
99
99
|
id: string;
|
|
@@ -143,93 +143,6 @@ export type CreateSchemaRequirements = {
|
|
|
143
143
|
resolved: string;
|
|
144
144
|
}[];
|
|
145
145
|
};
|
|
146
|
-
/**
|
|
147
|
-
* One field on an issue's edit screen, as Jira describes it.
|
|
148
|
-
*
|
|
149
|
-
* Normalized at the adapter: `operations` and `schema` come straight from
|
|
150
|
-
* Jira's own vocabulary because they are the vocabulary the decision is made
|
|
151
|
-
* in, but nothing else of the raw document travels.
|
|
152
|
-
*/
|
|
153
|
-
export type EditFieldMetadata = {
|
|
154
|
-
/** Jira's field id, e.g. `customfield_10021`. */
|
|
155
|
-
id: string;
|
|
156
|
-
name: string;
|
|
157
|
-
required: boolean;
|
|
158
|
-
/** What Jira says can be done to this field: `set`, `add`, `remove`, ... */
|
|
159
|
-
operations: string[];
|
|
160
|
-
schema: {
|
|
161
|
-
type: string;
|
|
162
|
-
/** Element type, for `type: "array"`. */
|
|
163
|
-
items?: string;
|
|
164
|
-
/** The custom field's implementation key, when it is a custom field. */
|
|
165
|
-
custom?: string;
|
|
166
|
-
customId?: number;
|
|
167
|
-
};
|
|
168
|
-
/** Present only where Jira constrains the value. Absent is not empty. */
|
|
169
|
-
allowedValues?: EditFieldOption[];
|
|
170
|
-
};
|
|
171
|
-
/**
|
|
172
|
-
* One option Jira offers for a constrained field.
|
|
173
|
-
*
|
|
174
|
-
* `id` is the identity and `label` is what a person reads - the same split as
|
|
175
|
-
* a user's accountId and display name, and for the same reason: an option can
|
|
176
|
-
* be renamed without becoming a different option, and two options could carry
|
|
177
|
-
* the same label.
|
|
178
|
-
*/
|
|
179
|
-
export type EditFieldOption = {
|
|
180
|
-
id: string;
|
|
181
|
-
label: string;
|
|
182
|
-
};
|
|
183
|
-
/**
|
|
184
|
-
* The custom field value families JAM can write.
|
|
185
|
-
*
|
|
186
|
-
* Narrow on purpose. Each of these has an unambiguous wire shape that JAM can
|
|
187
|
-
* produce from a plain caller value and compare after the fact. Everything
|
|
188
|
-
* else - dates needing a timezone policy, rich text needing ADF, user and
|
|
189
|
-
* group pickers needing identity resolution, app-owned fields with private
|
|
190
|
-
* semantics - is refused rather than guessed at.
|
|
191
|
-
*/
|
|
192
|
-
export declare const CUSTOM_FIELD_KINDS: readonly ["text", "number", "single-option", "multi-option"];
|
|
193
|
-
export type CustomFieldKind = (typeof CUSTOM_FIELD_KINDS)[number];
|
|
194
|
-
export type CustomFieldUpdateInput = {
|
|
195
|
-
/** A configured field id, or a configured writable field name. */
|
|
196
|
-
field: string;
|
|
197
|
-
value: string | number | string[];
|
|
198
|
-
};
|
|
199
|
-
/**
|
|
200
|
-
* What a custom-field plan depends on, recorded so apply can check it again.
|
|
201
|
-
*
|
|
202
|
-
* The issue's revision does not cover any of this: a field can be taken off a
|
|
203
|
-
* screen, lose its `set` operation, change type, or have an option renamed
|
|
204
|
-
* without the issue itself being touched. So these premises are frozen
|
|
205
|
-
* alongside `baseUpdated`, and re-derived before the write.
|
|
206
|
-
*/
|
|
207
|
-
export type CustomFieldRequirements = {
|
|
208
|
-
fieldId: string;
|
|
209
|
-
fieldName: string;
|
|
210
|
-
kind: CustomFieldKind;
|
|
211
|
-
schema: {
|
|
212
|
-
type: string;
|
|
213
|
-
items?: string;
|
|
214
|
-
custom?: string;
|
|
215
|
-
};
|
|
216
|
-
/** Options resolved from Jira's allowed list, for the option kinds. */
|
|
217
|
-
resolvedOptions?: EditFieldOption[];
|
|
218
|
-
};
|
|
219
|
-
/** A custom field value as a receipt shows it - reviewable, not a Jira payload. */
|
|
220
|
-
export type CustomFieldValueView = {
|
|
221
|
-
id: string;
|
|
222
|
-
name: string;
|
|
223
|
-
value: string | number | null;
|
|
224
|
-
} | {
|
|
225
|
-
id: string;
|
|
226
|
-
name: string;
|
|
227
|
-
value: EditFieldOption | null;
|
|
228
|
-
} | {
|
|
229
|
-
id: string;
|
|
230
|
-
name: string;
|
|
231
|
-
value: EditFieldOption[];
|
|
232
|
-
};
|
|
233
146
|
/** A transition as Jira currently offers it for one issue. */
|
|
234
147
|
export type JiraTransition = {
|
|
235
148
|
id: string;
|
|
@@ -275,11 +188,6 @@ export type ExistingIssueWritePlan = WritePlanCommon & {
|
|
|
275
188
|
* `undefined` means the issue was unassigned.
|
|
276
189
|
*/
|
|
277
190
|
baseAssigneeAccountId?: string;
|
|
278
|
-
/**
|
|
279
|
-
* What this plan assumed about a custom field's configuration. Present only
|
|
280
|
-
* for `custom-field.update`; apply re-derives each premise before writing.
|
|
281
|
-
*/
|
|
282
|
-
customFieldRequirements?: CustomFieldRequirements;
|
|
283
191
|
};
|
|
284
192
|
/**
|
|
285
193
|
* A plan to create an issue that does not exist yet.
|
|
@@ -314,10 +222,6 @@ export type WriteMutation = {
|
|
|
314
222
|
} | {
|
|
315
223
|
kind: "assignee";
|
|
316
224
|
accountId: string;
|
|
317
|
-
} | {
|
|
318
|
-
kind: "custom-field";
|
|
319
|
-
fieldId: string;
|
|
320
|
-
value: unknown;
|
|
321
225
|
} | {
|
|
322
226
|
kind: "create";
|
|
323
227
|
fields: Record<string, unknown>;
|
package/dist/domain/write.js
CHANGED
|
@@ -25,7 +25,6 @@ export const EXISTING_ISSUE_OPERATIONS = [
|
|
|
25
25
|
"field.update",
|
|
26
26
|
"status.transition",
|
|
27
27
|
"assignee.update",
|
|
28
|
-
"custom-field.update",
|
|
29
28
|
];
|
|
30
29
|
/** The operations the public MCP surface accepts. Nothing else is reachable. */
|
|
31
30
|
export const WRITE_OPERATIONS = [...EXISTING_ISSUE_OPERATIONS, "issue.create"];
|
|
@@ -56,16 +55,6 @@ export const CREATABLE_FIELDS = [
|
|
|
56
55
|
"labels",
|
|
57
56
|
"components",
|
|
58
57
|
];
|
|
59
|
-
/**
|
|
60
|
-
* The custom field value families JAM can write.
|
|
61
|
-
*
|
|
62
|
-
* Narrow on purpose. Each of these has an unambiguous wire shape that JAM can
|
|
63
|
-
* produce from a plain caller value and compare after the fact. Everything
|
|
64
|
-
* else - dates needing a timezone policy, rich text needing ADF, user and
|
|
65
|
-
* group pickers needing identity resolution, app-owned fields with private
|
|
66
|
-
* semantics - is refused rather than guessed at.
|
|
67
|
-
*/
|
|
68
|
-
export const CUSTOM_FIELD_KINDS = ["text", "number", "single-option", "multi-option"];
|
|
69
58
|
export function isWriteOperation(value) {
|
|
70
59
|
return WRITE_OPERATIONS.includes(value);
|
|
71
60
|
}
|
|
@@ -11,7 +11,6 @@ Operations on an issue that already exists - these need \`key\`:
|
|
|
11
11
|
- field.update input: { "summary"?, "priority"?, "labels"?, "components"? }
|
|
12
12
|
- status.transition input: { "status": "Done" } JAM asks Jira which transitions exist and matches yours
|
|
13
13
|
- assignee.update input: { "assignee": "..." } a display name or an accountId; JAM resolves it against Jira's own directory
|
|
14
|
-
- custom-field.update input: { "field": "...", "value": ... } one custom field the project opted in
|
|
15
14
|
|
|
16
15
|
Creating an issue - no \`key\`, because there is no issue yet:
|
|
17
16
|
- issue.create input: { "issueType": "Task", "summary": "...", "description"?, "priority"?, "labels"?, "components"? }
|
|
@@ -20,8 +19,6 @@ issue.create goes into the project this workspace is bound to; the project is no
|
|
|
20
19
|
|
|
21
20
|
assignee.update never sends the name you pass. JAM searches Jira's user directory, and assigns only when exactly one user matches your string exactly - an exact display name (case-insensitive) or an accountId. A partial match is Jira reporting a similarity, not identifying a person, so several matches or none come back as a refusal with the candidates attached: name one exactly, or pass their accountId. JAM also checks Jira offers that person as an assignee for this issue, before planning and again before writing, and confirms the result by accountId rather than by name. Unassigning, and setting an assignee while creating, are not in this version.
|
|
22
21
|
|
|
23
|
-
custom-field.update changes one custom field, and only one a team has opted in: the field's exact id must carry "writable: true" in the project's .jira-agent/project.yaml. Being readable does not make a field writable. "field" is that id or its configured name, matched exactly - no partial matches. JAM then asks Jira's edit metadata whether the field is settable on this issue for this account, and what type it is. Supported types are single-line text (string), number, single-select (string naming an option) and multi-select (array of strings). Anything else - dates, rich text, user or group pickers, app-owned fields - is refused rather than attempted. Types are not converted: "5" is not 5. Options are matched exactly against what Jira offers and written by option id. Clearing a field is not supported, so an empty string or an empty array is refused.
|
|
24
|
-
|
|
25
22
|
Writes are limited to the Jira project this workspace is bound to; a key from another project is refused rather than attempted.
|
|
26
23
|
|
|
27
24
|
The plan records what the issue looked like when it was made, and expires. If the issue changes in the meantime, jira_write_apply refuses with JAM_WRITE_CONFLICT - re-plan against the new state rather than forcing the old one through.
|
|
@@ -63,15 +60,6 @@ export function registerJiraWritePlan(server, deps) {
|
|
|
63
60
|
.min(1)
|
|
64
61
|
.optional()
|
|
65
62
|
.describe("assignee.update: who to assign, as an exact display name or an accountId. Not settable through field.update."),
|
|
66
|
-
field: z
|
|
67
|
-
.string()
|
|
68
|
-
.min(1)
|
|
69
|
-
.optional()
|
|
70
|
-
.describe("custom-field.update: the custom field, as its Jira id (customfield_10016) or its configured name. Must be writable in this project's config."),
|
|
71
|
-
value: z
|
|
72
|
-
.union([z.string(), z.number(), z.array(z.string())])
|
|
73
|
-
.optional()
|
|
74
|
-
.describe("custom-field.update: the value. A string for text, a number for numeric, a string naming an option for single-select, an array of strings for multi-select."),
|
|
75
63
|
issueType: z
|
|
76
64
|
.string()
|
|
77
65
|
.min(1)
|
|
@@ -38,16 +38,6 @@ export type GetIssueResult = {
|
|
|
38
38
|
* Absent when the issue is unassigned, or when the field was not requested.
|
|
39
39
|
*/
|
|
40
40
|
assigneeAccountId?: string;
|
|
41
|
-
/**
|
|
42
|
-
* Raw custom field values, keyed by field id, exactly as Jira returned them.
|
|
43
|
-
*
|
|
44
|
-
* Here rather than on the issue for the same reason as the accountId: only
|
|
45
|
-
* the write plane needs it. `FullIssueContext.customFields` is the mapped,
|
|
46
|
-
* whitelisted view the read tools show; verifying a write needs the value in
|
|
47
|
-
* the form Jira stores it, option ids included, before anything shortens it
|
|
48
|
-
* to something a person reads.
|
|
49
|
-
*/
|
|
50
|
-
customFieldValues?: Record<string, unknown>;
|
|
51
41
|
responseBytes: number;
|
|
52
42
|
};
|
|
53
43
|
export type GetIssuesResult = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jam-mcp/server",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "JAM (Jira Agent MCP) - agent-facing Jira access layer: MCP server, setup core, and CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jira",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"test:watch": "vitest"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@jam-mcp/launcher": "1.3.
|
|
44
|
+
"@jam-mcp/launcher": "1.3.2",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
46
46
|
"yaml": "^2.9.0",
|
|
47
47
|
"zod": "^4.4.3"
|