@jam-mcp/server 1.3.0 → 1.3.1

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.
@@ -2,27 +2,30 @@ import { z } from "zod";
2
2
  import { planWrite } from "../../application/plan-write.js";
3
3
  import { CREATABLE_FIELDS, WRITABLE_FIELDS, WRITE_OPERATIONS } from "../../domain/write.js";
4
4
  import { runTool } from "../tool-result.js";
5
- const DESCRIPTION = `Work out how to change one Jira issue, and get back a plan. Changes nothing.
6
-
7
- This is the first half of every write. Call it, read what it says the issue looks like now and what it would become, then pass the returned planId to jira_write_apply. There is no way to write to Jira without a plan, and a plan cannot be assembled by hand - only jira_write_plan issues one.
8
-
9
- Operations on an issue that already exists - these need \`key\`:
10
- - comment.add input: { "text": "..." } plain text; JAM converts it, do not send ADF
11
- - field.update input: { "summary"?, "priority"?, "labels"?, "components"? }
12
- - status.transition input: { "status": "Done" } JAM asks Jira which transitions exist and matches yours
13
- - assignee.update input: { "assignee": "..." } a display name or an accountId; JAM resolves it against Jira's own directory
14
-
15
- Creating an issue - no \`key\`, because there is no issue yet:
16
- - issue.create input: { "issueType": "Task", "summary": "...", "description"?, "priority"?, "labels"?, "components"? }
17
-
18
- issue.create goes into the project this workspace is bound to; the project is not a parameter. Planning reads Jira's create schema for that project first, so an issue type Jira does not offer, a priority or component outside its allowed values, and a project whose create screen requires a field JAM cannot set are all refused here rather than attempted. \`description\` is plain text, like a comment. Not settable in this version: assignee, reporter, parent, custom fields, attachments.
19
-
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.
21
-
22
- Writes are limited to the Jira project this workspace is bound to; a key from another project is refused rather than attempted.
23
-
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.
25
-
5
+ const DESCRIPTION = `Work out how to change one Jira issue, and get back a plan. Changes nothing.
6
+
7
+ This is the first half of every write. Call it, read what it says the issue looks like now and what it would become, then pass the returned planId to jira_write_apply. There is no way to write to Jira without a plan, and a plan cannot be assembled by hand - only jira_write_plan issues one.
8
+
9
+ Operations on an issue that already exists - these need \`key\`:
10
+ - comment.add input: { "text": "..." } plain text; JAM converts it, do not send ADF
11
+ - field.update input: { "summary"?, "priority"?, "labels"?, "components"? }
12
+ - status.transition input: { "status": "Done" } JAM asks Jira which transitions exist and matches yours
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
+
16
+ Creating an issue - no \`key\`, because there is no issue yet:
17
+ - issue.create input: { "issueType": "Task", "summary": "...", "description"?, "priority"?, "labels"?, "components"? }
18
+
19
+ issue.create goes into the project this workspace is bound to; the project is not a parameter. Planning reads Jira's create schema for that project first, so an issue type Jira does not offer, a priority or component outside its allowed values, and a project whose create screen requires a field JAM cannot set are all refused here rather than attempted. \`description\` is plain text, like a comment. Not settable in this version: assignee, reporter, parent, custom fields, attachments.
20
+
21
+ 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
+
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
+ Writes are limited to the Jira project this workspace is bound to; a key from another project is refused rather than attempted.
26
+
27
+ 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.
28
+
26
29
  A plan is a statement about what is possible right now, not a promise that it will happen. Nothing is written until jira_write_apply runs.`;
27
30
  export function registerJiraWritePlan(server, deps) {
28
31
  server.registerTool("jira_write_plan", {
@@ -60,6 +63,15 @@ export function registerJiraWritePlan(server, deps) {
60
63
  .min(1)
61
64
  .optional()
62
65
  .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."),
63
75
  issueType: z
64
76
  .string()
65
77
  .min(1)
@@ -0,0 +1,93 @@
1
+ import type { ProjectConfig } from "../config/schema.js";
2
+ import type { CustomFieldKind, CustomFieldRequirements, CustomFieldUpdateInput, CustomFieldValueView, EditFieldMetadata, EditFieldOption } from "../domain/write.js";
3
+ /**
4
+ * What JAM will write to a custom field, and everything that has to be true
5
+ * first.
6
+ *
7
+ * Three separate permissions have to line up, and none of them implies
8
+ * another:
9
+ *
10
+ * 1. **The team said so.** The field's exact id is in the project's whitelist
11
+ * with `writable: true`. Being readable is not being writable - reading a
12
+ * field and letting an agent change it are different decisions, and a
13
+ * config written before JAM could write must not start granting writes
14
+ * because JAM learned how.
15
+ * 2. **Jira allows it here and now.** The field is on this issue's edit
16
+ * screen for this account, and Jira lists `set` among its operations.
17
+ * Asked, never modelled: applicability depends on project, issue type,
18
+ * field contexts, screens and permissions, and JAM does not carry a copy
19
+ * of any of that.
20
+ * 3. **JAM knows the shape.** The field's type is one of four families whose
21
+ * wire form JAM can produce from a plain value and compare afterwards.
22
+ * Anything else is refused rather than posted to find out.
23
+ */
24
+ type WritableField = {
25
+ id: string;
26
+ name: string;
27
+ };
28
+ /**
29
+ * Which configured field this selector names.
30
+ *
31
+ * The id is the identity; the name is an alias for people. Resolution is exact
32
+ * on either - no substring, no fuzz - because the alternative is an agent's
33
+ * approximate word choosing which field on somebody's board gets rewritten.
34
+ *
35
+ * Only `writable: true` entries are candidates, including for the refusal
36
+ * message: naming a read-only field as an alternative would suggest it is one
37
+ * selector away from being written.
38
+ */
39
+ export declare function resolveWritableField(config: ProjectConfig, requested: string): WritableField;
40
+ /**
41
+ * The field as Jira currently offers it on this issue, or a refusal.
42
+ *
43
+ * Absent from the edit metadata and present-but-not-settable are different
44
+ * situations with the same answer for the caller, so they share a code and
45
+ * differ in the detail: one means the field is not on this screen, the other
46
+ * that Jira will not let this account set it.
47
+ */
48
+ export declare function assertEditable(issueKey: string, field: WritableField, metadata: EditFieldMetadata[]): EditFieldMetadata;
49
+ /**
50
+ * Which of the four families this field belongs to, if any.
51
+ *
52
+ * Classified from Jira's own `schema`, which is the vocabulary Jira answers
53
+ * in. The implementation key (`schema.custom`) deliberately does not decide
54
+ * it: there are hundreds of them, they are app-specific, and a field's wire
55
+ * shape follows its type rather than its plugin.
56
+ *
57
+ * Anything unclassified is refused. Posting an unknown type to see what
58
+ * happens would use a Jira 400 as schema discovery, and on the occasions it
59
+ * did not 400 it would write something nobody described.
60
+ */
61
+ export declare function classifyKind(field: EditFieldMetadata): CustomFieldKind;
62
+ /**
63
+ * The value, checked against the family and turned into what Jira expects.
64
+ *
65
+ * Types are never coerced. `"5"` is not `5`: a caller that meant a number can
66
+ * say so, and silently converting would make JAM's idea of the value differ
67
+ * from the caller's in exactly the cases where it matters.
68
+ *
69
+ * Nothing here clears a field. Empty strings, empty arrays and null are
70
+ * refused rather than treated as "unset" - removing a value is a different
71
+ * intent from setting one, and it is not in this version.
72
+ */
73
+ export declare function resolveCustomFieldValue(field: EditFieldMetadata, kind: CustomFieldKind, input: CustomFieldUpdateInput): {
74
+ jiraValue: unknown;
75
+ view: CustomFieldValueView;
76
+ resolvedOptions?: EditFieldOption[];
77
+ };
78
+ /**
79
+ * Do this plan's premises still hold?
80
+ *
81
+ * Semantic, like the create schema check and for the same reason: comparing
82
+ * whole metadata documents would invalidate every outstanding plan whenever an
83
+ * unrelated field appeared on the screen. What is compared is what the plan
84
+ * actually rested on - the field is still settable, still the same family,
85
+ * still the same schema, and every option it chose is still offered under the
86
+ * same label.
87
+ *
88
+ * A renamed option is treated as a changed one. The id is the identity, but a
89
+ * label is what the plan showed a human before they agreed to it, and "Backend"
90
+ * becoming "Platform" is a different statement about the issue.
91
+ */
92
+ export declare function assertCustomFieldUnchanged(issueKey: string, requirements: CustomFieldRequirements, metadata: EditFieldMetadata[]): void;
93
+ export {};
@@ -0,0 +1,230 @@
1
+ import { JamError } from "../domain/errors.js";
2
+ /**
3
+ * Which configured field this selector names.
4
+ *
5
+ * The id is the identity; the name is an alias for people. Resolution is exact
6
+ * on either - no substring, no fuzz - because the alternative is an agent's
7
+ * approximate word choosing which field on somebody's board gets rewritten.
8
+ *
9
+ * Only `writable: true` entries are candidates, including for the refusal
10
+ * message: naming a read-only field as an alternative would suggest it is one
11
+ * selector away from being written.
12
+ */
13
+ export function resolveWritableField(config, requested) {
14
+ const wanted = requested.trim();
15
+ if (wanted.length === 0) {
16
+ throw new JamError("JAM_WRITE_OPERATION_NOT_ALLOWED", "custom-field.update needs a non-empty `input.field`.", { operation: "custom-field.update" });
17
+ }
18
+ const writable = config.customFields.filter((f) => f.writable);
19
+ const match = writable.find((f) => f.id.toLowerCase() === wanted.toLowerCase()) ??
20
+ writable.find((f) => f.name.trim().toLowerCase() === wanted.toLowerCase());
21
+ if (!match) {
22
+ throw new JamError("JAM_WRITE_FIELD_NOT_ALLOWED", writable.length === 0
23
+ ? `No custom field in this project is writable. A team opts one in by adding \`writable: true\` to its entry in .jira-agent/project.yaml; being readable does not make a field writable.`
24
+ : `"${requested}" is not a writable custom field in this project. JAM writes only the exact ids a team has opted in.`, {
25
+ requested,
26
+ writableCustomFields: writable.map((f) => ({ id: f.id, name: f.name })),
27
+ });
28
+ }
29
+ return { id: match.id, name: match.name };
30
+ }
31
+ /**
32
+ * The field as Jira currently offers it on this issue, or a refusal.
33
+ *
34
+ * Absent from the edit metadata and present-but-not-settable are different
35
+ * situations with the same answer for the caller, so they share a code and
36
+ * differ in the detail: one means the field is not on this screen, the other
37
+ * that Jira will not let this account set it.
38
+ */
39
+ export function assertEditable(issueKey, field, metadata) {
40
+ const found = metadata.find((f) => f.id === field.id);
41
+ if (!found) {
42
+ throw new JamError("JAM_WRITE_CUSTOM_FIELD_NOT_EDITABLE", `Jira does not offer ${field.name} (${field.id}) on ${issueKey}'s edit screen for this account. The field may not apply to this project or issue type, or this account may not be able to edit it.`, { issueKey, fieldId: field.id, fieldName: field.name, reason: "NOT_ON_EDIT_SCREEN" });
43
+ }
44
+ if (!found.operations.includes("set")) {
45
+ throw new JamError("JAM_WRITE_CUSTOM_FIELD_NOT_EDITABLE", `Jira lists ${field.name} (${field.id}) on ${issueKey} but does not offer "set" for it${found.operations.length > 0 ? ` - only ${found.operations.join(", ")}` : ""}. JAM only sets a value; it does not add to or remove from one.`, {
46
+ issueKey,
47
+ fieldId: field.id,
48
+ fieldName: field.name,
49
+ operations: found.operations,
50
+ reason: "SET_NOT_OFFERED",
51
+ });
52
+ }
53
+ return found;
54
+ }
55
+ /**
56
+ * Which of the four families this field belongs to, if any.
57
+ *
58
+ * Classified from Jira's own `schema`, which is the vocabulary Jira answers
59
+ * in. The implementation key (`schema.custom`) deliberately does not decide
60
+ * it: there are hundreds of them, they are app-specific, and a field's wire
61
+ * shape follows its type rather than its plugin.
62
+ *
63
+ * Anything unclassified is refused. Posting an unknown type to see what
64
+ * happens would use a Jira 400 as schema discovery, and on the occasions it
65
+ * did not 400 it would write something nobody described.
66
+ */
67
+ export function classifyKind(field) {
68
+ const { type, items } = field.schema;
69
+ if (type === "string" && !items)
70
+ return "text";
71
+ if (type === "number" && !items)
72
+ return "number";
73
+ if (type === "option" && !items)
74
+ return "single-option";
75
+ if (type === "array" && items === "option")
76
+ return "multi-option";
77
+ throw new JamError("JAM_WRITE_CUSTOM_FIELD_TYPE_UNSUPPORTED", `${field.name} (${field.id}) is a ${describeType(field)} field, and JAM does not know how to write one safely yet. Supported: single-line text, number, single-select and multi-select.`, {
78
+ fieldId: field.id,
79
+ fieldName: field.name,
80
+ schema: field.schema,
81
+ supported: ["text", "number", "single-option", "multi-option"],
82
+ });
83
+ }
84
+ function describeType(field) {
85
+ const { type, items } = field.schema;
86
+ return items ? `${type} of ${items}` : type;
87
+ }
88
+ /**
89
+ * The value, checked against the family and turned into what Jira expects.
90
+ *
91
+ * Types are never coerced. `"5"` is not `5`: a caller that meant a number can
92
+ * say so, and silently converting would make JAM's idea of the value differ
93
+ * from the caller's in exactly the cases where it matters.
94
+ *
95
+ * Nothing here clears a field. Empty strings, empty arrays and null are
96
+ * refused rather than treated as "unset" - removing a value is a different
97
+ * intent from setting one, and it is not in this version.
98
+ */
99
+ export function resolveCustomFieldValue(field, kind, input) {
100
+ const { value } = input;
101
+ const named = { id: field.id, name: field.name };
102
+ switch (kind) {
103
+ case "text": {
104
+ if (typeof value !== "string")
105
+ throw wrongType(field, kind, value);
106
+ const text = value.trim();
107
+ if (text.length === 0)
108
+ throw refuseClear(field);
109
+ return { jiraValue: text, view: { ...named, value: text } };
110
+ }
111
+ case "number": {
112
+ if (typeof value !== "number" || !Number.isFinite(value))
113
+ throw wrongType(field, kind, value);
114
+ return { jiraValue: value, view: { ...named, value } };
115
+ }
116
+ case "single-option": {
117
+ if (typeof value !== "string")
118
+ throw wrongType(field, kind, value);
119
+ const option = resolveOption(field, value);
120
+ // Jira takes the option by id. The label is what a person reads, and two
121
+ // options could carry the same one.
122
+ return {
123
+ jiraValue: { id: option.id },
124
+ view: { ...named, value: option },
125
+ resolvedOptions: [option],
126
+ };
127
+ }
128
+ case "multi-option": {
129
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
130
+ throw wrongType(field, kind, value);
131
+ }
132
+ if (value.length === 0)
133
+ throw refuseClear(field);
134
+ const seen = new Set();
135
+ for (const raw of value) {
136
+ const key = raw.trim().toLowerCase();
137
+ if (seen.has(key)) {
138
+ throw new JamError("JAM_WRITE_VALUE_NOT_ALLOWED", `"${raw}" appears more than once in the value for ${field.name}. JAM does not quietly drop the repeat - say each option once.`, { fieldId: field.id, repeated: raw });
139
+ }
140
+ seen.add(key);
141
+ }
142
+ // Every option resolves, or none is written. A partly-applied selection
143
+ // is a selection nobody asked for.
144
+ const options = value.map((raw) => resolveOption(field, raw));
145
+ return {
146
+ jiraValue: options.map((o) => ({ id: o.id })),
147
+ view: { ...named, value: options },
148
+ resolvedOptions: options,
149
+ };
150
+ }
151
+ }
152
+ }
153
+ /**
154
+ * Which option Jira offers under this name, if exactly one does.
155
+ *
156
+ * An option id wins outright, then an exact label ignoring case and space.
157
+ * Nothing partial: Jira's option lists are short and a caller can name one
158
+ * exactly, so a near miss is a question rather than a guess.
159
+ */
160
+ function resolveOption(field, requested) {
161
+ const allowed = field.allowedValues;
162
+ if (!allowed) {
163
+ throw new JamError("JAM_WRITE_CUSTOM_FIELD_TYPE_UNSUPPORTED", `${field.name} (${field.id}) is a select field, but Jira did not say which options it offers, so JAM cannot resolve "${requested}" to one.`, { fieldId: field.id, fieldName: field.name, schema: field.schema });
164
+ }
165
+ const wanted = requested.trim();
166
+ const byId = allowed.filter((o) => o.id === wanted);
167
+ const matches = byId.length > 0
168
+ ? byId
169
+ : allowed.filter((o) => o.label.trim().toLowerCase() === wanted.toLowerCase());
170
+ if (matches.length === 0) {
171
+ throw new JamError("JAM_WRITE_VALUE_NOT_ALLOWED", allowed.length === 0
172
+ ? `Jira offers no options for ${field.name} on this issue, so "${requested}" cannot be set.`
173
+ : `"${requested}" is not an option Jira offers for ${field.name}. Allowed: ${allowed.map((o) => o.label).join(", ")}.`, { fieldId: field.id, requested, allowed });
174
+ }
175
+ if (matches.length > 1) {
176
+ throw new JamError("JAM_WRITE_VALUE_NOT_ALLOWED", `"${requested}" matches ${matches.length} options for ${field.name}. Pass the option id of the one you mean.`, { fieldId: field.id, requested, candidates: matches });
177
+ }
178
+ return matches[0];
179
+ }
180
+ /**
181
+ * Do this plan's premises still hold?
182
+ *
183
+ * Semantic, like the create schema check and for the same reason: comparing
184
+ * whole metadata documents would invalidate every outstanding plan whenever an
185
+ * unrelated field appeared on the screen. What is compared is what the plan
186
+ * actually rested on - the field is still settable, still the same family,
187
+ * still the same schema, and every option it chose is still offered under the
188
+ * same label.
189
+ *
190
+ * A renamed option is treated as a changed one. The id is the identity, but a
191
+ * label is what the plan showed a human before they agreed to it, and "Backend"
192
+ * becoming "Platform" is a different statement about the issue.
193
+ */
194
+ export function assertCustomFieldUnchanged(issueKey, requirements, metadata) {
195
+ const field = metadata.find((f) => f.id === requirements.fieldId);
196
+ if (!field) {
197
+ throw schemaChanged(`${requirements.fieldName} (${requirements.fieldId}) is no longer on ${issueKey}'s edit screen for this account.`, { issueKey, fieldId: requirements.fieldId });
198
+ }
199
+ if (!field.operations.includes("set")) {
200
+ throw schemaChanged(`Jira no longer offers "set" for ${requirements.fieldName} on ${issueKey}.`, { issueKey, fieldId: field.id, operations: field.operations });
201
+ }
202
+ if (field.schema.type !== requirements.schema.type ||
203
+ field.schema.items !== requirements.schema.items) {
204
+ throw schemaChanged(`${requirements.fieldName} is no longer a ${requirements.kind} field.`, { issueKey, fieldId: field.id, planned: requirements.schema, current: field.schema });
205
+ }
206
+ for (const planned of requirements.resolvedOptions ?? []) {
207
+ const current = field.allowedValues?.find((o) => o.id === planned.id);
208
+ if (!current) {
209
+ throw schemaChanged(`Option "${planned.label}" is no longer offered for ${requirements.fieldName}.`, { issueKey, fieldId: field.id, option: planned });
210
+ }
211
+ if (current.label !== planned.label) {
212
+ throw schemaChanged(`Option "${planned.label}" has been renamed to "${current.label}", so this plan no longer describes the change it showed.`, { issueKey, fieldId: field.id, planned, current });
213
+ }
214
+ }
215
+ }
216
+ function schemaChanged(what, details) {
217
+ return new JamError("JAM_WRITE_SCHEMA_CHANGED", `${what} This plan was built on the field's configuration as it was, so it no longer describes a change JAM can make. Nothing was written - plan again.`, details);
218
+ }
219
+ function wrongType(field, kind, value) {
220
+ const wanted = {
221
+ text: "a string",
222
+ number: "a number",
223
+ "single-option": "a string naming one option",
224
+ "multi-option": "an array of strings naming options",
225
+ }[kind];
226
+ return new JamError("JAM_WRITE_VALUE_NOT_ALLOWED", `${field.name} (${field.id}) is a ${kind} field and needs ${wanted}. JAM does not convert between types - "5" and 5 are different values, and guessing which was meant is not JAM's to do.`, { fieldId: field.id, kind, received: typeof value });
227
+ }
228
+ function refuseClear(field) {
229
+ return new JamError("JAM_WRITE_VALUE_NOT_ALLOWED", `custom-field.update sets a value; it does not clear one. ${field.name} cannot be set to an empty value in this version.`, { fieldId: field.id, reason: "CLEAR_NOT_SUPPORTED" });
230
+ }
@@ -0,0 +1,22 @@
1
+ import type { EditFieldMetadata } from "../domain/write.js";
2
+ /**
3
+ * What Jira will let this account change on one issue, right now.
4
+ *
5
+ * `GET /rest/api/3/issue/{key}/editmeta` is the authority, and it is asked
6
+ * rather than reconstructed. A custom field's applicability depends on the
7
+ * project, the issue type, the field's contexts, the screen it is on and the
8
+ * permissions of whoever is asking - JAM does not carry a copy of that model,
9
+ * and the field-context APIs that would let it try need administrator rights
10
+ * most tokens do not have. So the question is put to Jira in the form it can
11
+ * answer exactly: on this issue, for this account, what is editable and how.
12
+ *
13
+ * The same shape as the other read-shaped ports, for the same reasons: it
14
+ * mutates nothing, so it does not belong behind the write port's no-retry
15
+ * contract, and it answers a question about a configuration rather than about
16
+ * an issue, so the read port's completeness semantics would mean nothing here.
17
+ *
18
+ * It does not retry. Its answer decides a mutation.
19
+ */
20
+ export interface JiraEditMetadataPort {
21
+ getEditableFields(issueKey: string): Promise<EditFieldMetadata[]>;
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -38,6 +38,16 @@ 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>;
41
51
  responseBytes: number;
42
52
  };
43
53
  export type GetIssuesResult = {
package/package.json CHANGED
@@ -1,69 +1,69 @@
1
- {
2
- "name": "@jam-mcp/server",
3
- "version": "1.3.0",
4
- "description": "JAM (Jira Agent MCP) - agent-facing Jira access layer: MCP server, setup core, and CLI",
5
- "keywords": [
6
- "jira",
7
- "mcp",
8
- "model-context-protocol",
9
- "claude-code",
10
- "codex",
11
- "jira-api"
12
- ],
13
- "homepage": "https://github.com/colosair/jam#readme",
14
- "bugs": {
15
- "url": "https://github.com/colosair/jam/issues"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/colosair/jam.git",
20
- "directory": "packages/server"
21
- },
22
- "author": "colosair (https://github.com/colosair)",
23
- "type": "module",
24
- "bin": {
25
- "jam-server": "dist/index.js"
26
- },
27
- "main": "dist/index.js",
28
- "types": "dist/index.d.ts",
29
- "engines": {
30
- "node": ">=20"
31
- },
32
- "files": [
33
- "dist",
34
- "!dist/**/*.js.map",
35
- "README.md"
36
- ],
37
- "scripts": {
38
- "build": "tsc",
39
- "dev": "tsc --watch",
40
- "test": "vitest run",
41
- "test:watch": "vitest"
42
- },
43
- "dependencies": {
44
- "@jam-mcp/launcher": "1.3.0",
45
- "@modelcontextprotocol/sdk": "^1.30.0",
46
- "yaml": "^2.9.0",
47
- "zod": "^4.4.3"
48
- },
49
- "devDependencies": {
50
- "@types/node": "^24.0.0",
51
- "typescript": "^5.9.0",
52
- "vitest": "^4.1.11"
53
- },
54
- "license": "MIT",
55
- "exports": {
56
- ".": {
57
- "types": "./dist/index.d.ts",
58
- "default": "./dist/index.js"
59
- },
60
- "./cli-entry": {
61
- "types": "./dist/cli-entry.d.ts",
62
- "default": "./dist/cli-entry.js"
63
- }
64
- },
65
- "publishConfig": {
66
- "access": "public",
67
- "registry": "https://registry.npmjs.org/"
68
- }
69
- }
1
+ {
2
+ "name": "@jam-mcp/server",
3
+ "version": "1.3.1",
4
+ "description": "JAM (Jira Agent MCP) - agent-facing Jira access layer: MCP server, setup core, and CLI",
5
+ "keywords": [
6
+ "jira",
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "claude-code",
10
+ "codex",
11
+ "jira-api"
12
+ ],
13
+ "homepage": "https://github.com/colosair/jam#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/colosair/jam/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/colosair/jam.git",
20
+ "directory": "packages/server"
21
+ },
22
+ "author": "colosair (https://github.com/colosair)",
23
+ "type": "module",
24
+ "bin": {
25
+ "jam-server": "dist/index.js"
26
+ },
27
+ "main": "dist/index.js",
28
+ "types": "dist/index.d.ts",
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "!dist/**/*.js.map",
35
+ "README.md"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "dev": "tsc --watch",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest"
42
+ },
43
+ "dependencies": {
44
+ "@jam-mcp/launcher": "1.3.1",
45
+ "@modelcontextprotocol/sdk": "^1.30.0",
46
+ "yaml": "^2.9.0",
47
+ "zod": "^4.4.3"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^24.0.0",
51
+ "typescript": "^5.9.0",
52
+ "vitest": "^4.1.11"
53
+ },
54
+ "license": "MIT",
55
+ "exports": {
56
+ ".": {
57
+ "types": "./dist/index.d.ts",
58
+ "default": "./dist/index.js"
59
+ },
60
+ "./cli-entry": {
61
+ "types": "./dist/cli-entry.d.ts",
62
+ "default": "./dist/cli-entry.js"
63
+ }
64
+ },
65
+ "publishConfig": {
66
+ "access": "public",
67
+ "registry": "https://registry.npmjs.org/"
68
+ }
69
+ }