@jam-mcp/server 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/adapters/credentials/secret-store.d.ts +7 -2
- package/dist/adapters/credentials/secret-store.js +62 -10
- 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/live-toolset.d.ts +36 -0
- package/dist/bootstrap/live-toolset.js +85 -0
- package/dist/bootstrap/mcp-config-merger.d.ts +11 -4
- package/dist/bootstrap/mcp-config-merger.js +10 -5
- 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/dist/adapters/jira-cloud/jira-edit-metadata.adapter.d.ts +0 -25
- package/dist/adapters/jira-cloud/jira-edit-metadata.adapter.js +0 -84
- package/dist/policy/custom-field-policy.d.ts +0 -93
- package/dist/policy/custom-field-policy.js +0 -230
- package/dist/ports/jira-edit-metadata.port.d.ts +0 -22
- package/dist/ports/jira-edit-metadata.port.js +0 -1
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
|
+
"version": "1.4.0",
|
|
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.
|
|
44
|
+
"@jam-mcp/launcher": "1.4.0",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
46
46
|
"yaml": "^2.9.0",
|
|
47
47
|
"zod": "^4.4.3"
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { EditFieldMetadata } from "../../domain/write.js";
|
|
2
|
-
import type { CredentialPort } from "../../ports/credentials.port.js";
|
|
3
|
-
import type { JiraEditMetadataPort } from "../../ports/jira-edit-metadata.port.js";
|
|
4
|
-
/**
|
|
5
|
-
* Jira Cloud REST v3 edit metadata for one issue.
|
|
6
|
-
*
|
|
7
|
-
* `GET /rest/api/3/issue/{key}/editmeta`, `retry: false`. Its answer decides
|
|
8
|
-
* whether a mutation may proceed and what shape it takes, so a
|
|
9
|
-
* retried-and-stale answer is worse than a failure - the same argument that
|
|
10
|
-
* keeps `getTransitions`, the create metadata calls and the assignability
|
|
11
|
-
* check on the non-retrying side.
|
|
12
|
-
*
|
|
13
|
-
* Jira keys the response by field id, and describes each field with a `schema`
|
|
14
|
-
* and a list of `operations`. Both travel, because both are what the decision
|
|
15
|
-
* is made in; the rest of the document does not.
|
|
16
|
-
*
|
|
17
|
-
* Anything JAM cannot read is dropped rather than half-understood. A field
|
|
18
|
-
* that survives here with the wrong shape would be a field JAM claims to
|
|
19
|
-
* understand well enough to write.
|
|
20
|
-
*/
|
|
21
|
-
export declare class JiraCloudEditMetadataAdapter implements JiraEditMetadataPort {
|
|
22
|
-
private readonly client;
|
|
23
|
-
constructor(credentials: CredentialPort, fetchImpl?: typeof fetch);
|
|
24
|
-
getEditableFields(issueKey: string): Promise<EditFieldMetadata[]>;
|
|
25
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { JiraClient } from "./jira-client.js";
|
|
2
|
-
/**
|
|
3
|
-
* Jira Cloud REST v3 edit metadata for one issue.
|
|
4
|
-
*
|
|
5
|
-
* `GET /rest/api/3/issue/{key}/editmeta`, `retry: false`. Its answer decides
|
|
6
|
-
* whether a mutation may proceed and what shape it takes, so a
|
|
7
|
-
* retried-and-stale answer is worse than a failure - the same argument that
|
|
8
|
-
* keeps `getTransitions`, the create metadata calls and the assignability
|
|
9
|
-
* check on the non-retrying side.
|
|
10
|
-
*
|
|
11
|
-
* Jira keys the response by field id, and describes each field with a `schema`
|
|
12
|
-
* and a list of `operations`. Both travel, because both are what the decision
|
|
13
|
-
* is made in; the rest of the document does not.
|
|
14
|
-
*
|
|
15
|
-
* Anything JAM cannot read is dropped rather than half-understood. A field
|
|
16
|
-
* that survives here with the wrong shape would be a field JAM claims to
|
|
17
|
-
* understand well enough to write.
|
|
18
|
-
*/
|
|
19
|
-
export class JiraCloudEditMetadataAdapter {
|
|
20
|
-
client;
|
|
21
|
-
constructor(credentials, fetchImpl) {
|
|
22
|
-
this.client = fetchImpl ? new JiraClient(credentials, fetchImpl) : new JiraClient(credentials);
|
|
23
|
-
}
|
|
24
|
-
async getEditableFields(issueKey) {
|
|
25
|
-
const { data } = await this.client.request({
|
|
26
|
-
path: `rest/api/3/issue/${encodeURIComponent(issueKey)}/editmeta`,
|
|
27
|
-
retry: false,
|
|
28
|
-
});
|
|
29
|
-
const fields = data?.fields;
|
|
30
|
-
if (!fields || typeof fields !== "object")
|
|
31
|
-
return [];
|
|
32
|
-
return Object.entries(fields)
|
|
33
|
-
.map(([id, raw]) => toEditField(id, raw))
|
|
34
|
-
.filter((f) => f !== undefined);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function toEditField(id, raw) {
|
|
38
|
-
if (!raw || typeof raw !== "object")
|
|
39
|
-
return undefined;
|
|
40
|
-
// A field with no schema type is a field JAM cannot classify, and an
|
|
41
|
-
// unclassifiable field is one it must not decide it can write.
|
|
42
|
-
const type = typeof raw.schema?.type === "string" ? raw.schema.type : undefined;
|
|
43
|
-
if (!type)
|
|
44
|
-
return undefined;
|
|
45
|
-
const allowed = toOptions(raw.allowedValues);
|
|
46
|
-
return {
|
|
47
|
-
id,
|
|
48
|
-
name: typeof raw.name === "string" ? raw.name : id,
|
|
49
|
-
required: raw.required === true,
|
|
50
|
-
operations: Array.isArray(raw.operations)
|
|
51
|
-
? raw.operations.filter((op) => typeof op === "string")
|
|
52
|
-
: [],
|
|
53
|
-
schema: {
|
|
54
|
-
type,
|
|
55
|
-
...(typeof raw.schema?.items === "string" ? { items: raw.schema.items } : {}),
|
|
56
|
-
...(typeof raw.schema?.custom === "string" ? { custom: raw.schema.custom } : {}),
|
|
57
|
-
...(typeof raw.schema?.customId === "number" ? { customId: raw.schema.customId } : {}),
|
|
58
|
-
},
|
|
59
|
-
...(allowed ? { allowedValues: allowed } : {}),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* The options Jira offers, when it constrains the field at all.
|
|
64
|
-
*
|
|
65
|
-
* Absent and empty mean different things and stay apart: absent is "Jira did
|
|
66
|
-
* not constrain this", empty is "Jira constrains it and offers nothing". The
|
|
67
|
-
* first permits a free value, the second permits none.
|
|
68
|
-
*
|
|
69
|
-
* Jira labels an option `value` on a select and `name` on some other pickers.
|
|
70
|
-
* Both are read; an option with neither an id nor a label is dropped, because
|
|
71
|
-
* it can be neither chosen nor recognised afterwards.
|
|
72
|
-
*/
|
|
73
|
-
function toOptions(raw) {
|
|
74
|
-
if (!Array.isArray(raw))
|
|
75
|
-
return undefined;
|
|
76
|
-
return raw
|
|
77
|
-
.map((entry) => {
|
|
78
|
-
const o = entry;
|
|
79
|
-
const id = typeof o?.id === "string" ? o.id : typeof o?.id === "number" ? String(o.id) : undefined;
|
|
80
|
-
const label = typeof o?.value === "string" ? o.value : typeof o?.name === "string" ? o.name : undefined;
|
|
81
|
-
return id && label ? { id, label } : undefined;
|
|
82
|
-
})
|
|
83
|
-
.filter((o) => o !== undefined);
|
|
84
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
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 {};
|