@jam-mcp/server 1.2.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.
Files changed (39) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +90 -86
  3. package/dist/adapters/jira-cloud/jira-assignee-resolution.adapter.d.ts +39 -0
  4. package/dist/adapters/jira-cloud/jira-assignee-resolution.adapter.js +94 -0
  5. package/dist/adapters/jira-cloud/jira-edit-metadata.adapter.d.ts +25 -0
  6. package/dist/adapters/jira-cloud/jira-edit-metadata.adapter.js +84 -0
  7. package/dist/adapters/jira-cloud/jira-read.adapter.js +20 -1
  8. package/dist/adapters/jira-cloud/jira-write.adapter.d.ts +9 -0
  9. package/dist/adapters/jira-cloud/jira-write.adapter.js +16 -0
  10. package/dist/application/apply-create-issue.js +1 -1
  11. package/dist/application/apply-write.js +86 -3
  12. package/dist/application/plan-write.d.ts +20 -2
  13. package/dist/application/plan-write.js +166 -18
  14. package/dist/bootstrap/mcp-config-merger.d.ts +1 -1
  15. package/dist/bootstrap/setup-plan.d.ts +11 -0
  16. package/dist/bootstrap/setup-plan.js +10 -1
  17. package/dist/cli-entry.js +33 -33
  18. package/dist/config/schema.d.ts +1 -0
  19. package/dist/config/schema.js +35 -1
  20. package/dist/deps.d.ts +18 -0
  21. package/dist/deps.js +12 -0
  22. package/dist/domain/errors.d.ts +1 -1
  23. package/dist/domain/errors.js +14 -0
  24. package/dist/domain/write.d.ts +136 -3
  25. package/dist/domain/write.js +12 -0
  26. package/dist/index.js +0 -0
  27. package/dist/mcp/tools/jira-write-apply.tool.js +14 -14
  28. package/dist/mcp/tools/jira-write-plan.tool.js +40 -20
  29. package/dist/policy/assignee-policy.d.ts +60 -0
  30. package/dist/policy/assignee-policy.js +103 -0
  31. package/dist/policy/custom-field-policy.d.ts +93 -0
  32. package/dist/policy/custom-field-policy.js +230 -0
  33. package/dist/ports/jira-assignee-resolution.port.d.ts +51 -0
  34. package/dist/ports/jira-assignee-resolution.port.js +1 -0
  35. package/dist/ports/jira-edit-metadata.port.d.ts +22 -0
  36. package/dist/ports/jira-edit-metadata.port.js +1 -0
  37. package/dist/ports/jira-read.port.d.ts +21 -0
  38. package/dist/ports/jira-write.port.d.ts +8 -0
  39. package/package.json +69 -69
@@ -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,51 @@
1
+ import type { AssigneeCandidate } from "../domain/write.js";
2
+ /**
3
+ * Who Jira thinks a name refers to, and whether they can hold this issue.
4
+ *
5
+ * A third read-shaped port, for the same reason `JiraCreateMetadataPort` is
6
+ * one: nothing here mutates, so it does not belong behind the write port's
7
+ * no-retry contract, and it answers a question about a directory rather than
8
+ * about an issue, so the read port's completeness semantics would mean nothing
9
+ * for it.
10
+ *
11
+ * The two calls are deliberately separate questions. Searching answers "who
12
+ * did the caller mean", and it is allowed to be fuzzy - Jira matches on
13
+ * substrings, and a partial match is a suggestion to show a human. Checking
14
+ * assignability answers "may this exact person hold this exact issue", and it
15
+ * is not fuzzy at all: it takes an accountId that resolution has already
16
+ * settled on. Collapsing them would let a substring match decide a mutation.
17
+ *
18
+ * Neither call retries. Their answers decide a mutation, and a retried answer
19
+ * is a possibly-stale one - the same argument that keeps `getTransitions` and
20
+ * the create metadata calls on the non-retrying side.
21
+ */
22
+ export interface JiraAssigneeResolutionPort {
23
+ /**
24
+ * Users matching a query, as Jira's own directory reports them.
25
+ *
26
+ * Fuzzy by nature. What comes back is candidates, never a decision - see
27
+ * `resolveAssignee` for what JAM will and will not do with them.
28
+ */
29
+ searchUsers(query: string): Promise<AssigneeCandidate[]>;
30
+ /**
31
+ * One user, looked up by identity rather than found by searching.
32
+ *
33
+ * JAM's contract says `assignee` may be an accountId, and a contract about
34
+ * identity has to be met by an identity lookup. Jira's user search does
35
+ * currently return a user when the query happens to be their accountId, but
36
+ * that is a property of a substring search, not a promise - relying on it
37
+ * means the accountId half of the contract holds by coincidence.
38
+ *
39
+ * Absent means Jira has no such account, or this token cannot see it. Both
40
+ * are "you cannot assign this", which is the caller's answer either way.
41
+ */
42
+ getUserByAccountId(accountId: string): Promise<AssigneeCandidate | undefined>;
43
+ /**
44
+ * Whether this exact account may be assigned this exact issue, right now.
45
+ *
46
+ * Asked by accountId, so it is an identity question rather than a name one.
47
+ * Asked again immediately before the write, because a permission that held
48
+ * when the plan was made is not the same as one that still holds.
49
+ */
50
+ isAssignable(issueKey: string, accountId: string): Promise<boolean>;
51
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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 {};
@@ -27,6 +27,27 @@ export type GetIssueRequest = {
27
27
  export type GetIssueResult = {
28
28
  /** Absent when Jira has no such issue, or this account cannot see it. */
29
29
  issue?: FullIssueContext;
30
+ /**
31
+ * Who the issue is assigned to, by identity rather than by name.
32
+ *
33
+ * Here rather than on the issue because only the write plane needs it. Two
34
+ * people can share a display name, so `assignee` cannot settle whether an
35
+ * assignment landed on the right person - and the read tools have no use for
36
+ * an accountId, so their payload does not grow one.
37
+ *
38
+ * Absent when the issue is unassigned, or when the field was not requested.
39
+ */
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>;
30
51
  responseBytes: number;
31
52
  };
32
53
  export type GetIssuesResult = {
@@ -32,4 +32,12 @@ export interface JiraWritePort {
32
32
  /** Transitions Jira offers for this issue right now, for this account. */
33
33
  getTransitions(key: string): Promise<JiraTransition[]>;
34
34
  transitionIssue(key: string, transitionId: string): Promise<void>;
35
+ /**
36
+ * Assign an issue to one account.
37
+ *
38
+ * By accountId, never by name: Jira Cloud identifies users by account, and a
39
+ * display name is a label two people can share. Which account it is was
40
+ * settled during planning.
41
+ */
42
+ assignIssue(key: string, accountId: string): Promise<void>;
35
43
  }
package/package.json CHANGED
@@ -1,69 +1,69 @@
1
- {
2
- "name": "@jam-mcp/server",
3
- "version": "1.2.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.2.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
+ }