@jam-mcp/server 1.0.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/adapters/cache/noop-cache.d.ts +7 -0
  4. package/dist/adapters/cache/noop-cache.js +12 -0
  5. package/dist/adapters/credentials/composite.d.ts +30 -0
  6. package/dist/adapters/credentials/composite.js +79 -0
  7. package/dist/adapters/credentials/process-env.d.ts +13 -0
  8. package/dist/adapters/credentials/process-env.js +17 -0
  9. package/dist/adapters/credentials/secret-store.d.ts +69 -0
  10. package/dist/adapters/credentials/secret-store.js +259 -0
  11. package/dist/adapters/credentials/windows-user-env.d.ts +20 -0
  12. package/dist/adapters/credentials/windows-user-env.js +51 -0
  13. package/dist/adapters/jira-cloud/adf-to-text.d.ts +12 -0
  14. package/dist/adapters/jira-cloud/adf-to-text.js +151 -0
  15. package/dist/adapters/jira-cloud/jira-client.d.ts +32 -0
  16. package/dist/adapters/jira-cloud/jira-client.js +137 -0
  17. package/dist/adapters/jira-cloud/jira-read.adapter.d.ts +13 -0
  18. package/dist/adapters/jira-cloud/jira-read.adapter.js +97 -0
  19. package/dist/adapters/jira-cloud/jira-write.adapter.d.ts +12 -0
  20. package/dist/adapters/jira-cloud/jira-write.adapter.js +17 -0
  21. package/dist/adapters/jira-cloud/mapper.d.ts +34 -0
  22. package/dist/adapters/jira-cloud/mapper.js +143 -0
  23. package/dist/adapters/telemetry/console-telemetry.d.ts +13 -0
  24. package/dist/adapters/telemetry/console-telemetry.js +31 -0
  25. package/dist/application/get-full-issue-context.d.ts +18 -0
  26. package/dist/application/get-full-issue-context.js +120 -0
  27. package/dist/application/get-issue-context.d.ts +19 -0
  28. package/dist/application/get-issue-context.js +79 -0
  29. package/dist/application/search-issues.d.ts +22 -0
  30. package/dist/application/search-issues.js +93 -0
  31. package/dist/bootstrap/boot-health-gate.d.ts +25 -0
  32. package/dist/bootstrap/boot-health-gate.js +137 -0
  33. package/dist/bootstrap/bootstrap-orchestrator.d.ts +15 -0
  34. package/dist/bootstrap/bootstrap-orchestrator.js +14 -0
  35. package/dist/bootstrap/host-mcp.d.ts +54 -0
  36. package/dist/bootstrap/host-mcp.js +98 -0
  37. package/dist/bootstrap/jira-projects.d.ts +16 -0
  38. package/dist/bootstrap/jira-projects.js +24 -0
  39. package/dist/bootstrap/mcp-config-merger.d.ts +58 -0
  40. package/dist/bootstrap/mcp-config-merger.js +119 -0
  41. package/dist/bootstrap/migration-target.d.ts +60 -0
  42. package/dist/bootstrap/migration-target.js +80 -0
  43. package/dist/bootstrap/project-bindings.d.ts +48 -0
  44. package/dist/bootstrap/project-bindings.js +112 -0
  45. package/dist/bootstrap/project-config-bootstrapper.d.ts +37 -0
  46. package/dist/bootstrap/project-config-bootstrapper.js +69 -0
  47. package/dist/bootstrap/project-config-resolver.d.ts +53 -0
  48. package/dist/bootstrap/project-config-resolver.js +59 -0
  49. package/dist/bootstrap/project-root-resolver.d.ts +22 -0
  50. package/dist/bootstrap/project-root-resolver.js +43 -0
  51. package/dist/bootstrap/setup-apply.d.ts +31 -0
  52. package/dist/bootstrap/setup-apply.js +61 -0
  53. package/dist/bootstrap/setup-plan.d.ts +102 -0
  54. package/dist/bootstrap/setup-plan.js +224 -0
  55. package/dist/bootstrap/setup-state.d.ts +73 -0
  56. package/dist/bootstrap/setup-state.js +89 -0
  57. package/dist/bootstrap/workspace-identity.d.ts +52 -0
  58. package/dist/bootstrap/workspace-identity.js +114 -0
  59. package/dist/cli/agent-api.d.ts +63 -0
  60. package/dist/cli/agent-api.js +149 -0
  61. package/dist/cli/auth.d.ts +39 -0
  62. package/dist/cli/auth.js +201 -0
  63. package/dist/cli/doctor.d.ts +7 -0
  64. package/dist/cli/doctor.js +31 -0
  65. package/dist/cli/runtime.d.ts +16 -0
  66. package/dist/cli/runtime.js +97 -0
  67. package/dist/cli/serve.d.ts +7 -0
  68. package/dist/cli/serve.js +23 -0
  69. package/dist/cli/setup-wizard.d.ts +37 -0
  70. package/dist/cli/setup-wizard.js +338 -0
  71. package/dist/cli/setup.d.ts +26 -0
  72. package/dist/cli/setup.js +214 -0
  73. package/dist/cli/ui.d.ts +118 -0
  74. package/dist/cli/ui.js +338 -0
  75. package/dist/cli-entry.d.ts +7 -0
  76. package/dist/cli-entry.js +124 -0
  77. package/dist/config/load-config.d.ts +18 -0
  78. package/dist/config/load-config.js +48 -0
  79. package/dist/config/schema.d.ts +36 -0
  80. package/dist/config/schema.js +74 -0
  81. package/dist/deps.d.ts +50 -0
  82. package/dist/deps.js +39 -0
  83. package/dist/domain/completeness.d.ts +63 -0
  84. package/dist/domain/completeness.js +20 -0
  85. package/dist/domain/context.d.ts +38 -0
  86. package/dist/domain/context.js +1 -0
  87. package/dist/domain/errors.d.ts +27 -0
  88. package/dist/domain/errors.js +49 -0
  89. package/dist/domain/issue.d.ts +35 -0
  90. package/dist/domain/issue.js +1 -0
  91. package/dist/index.d.ts +2 -0
  92. package/dist/index.js +17 -0
  93. package/dist/mcp/create-server.d.ts +9 -0
  94. package/dist/mcp/create-server.js +32 -0
  95. package/dist/mcp/tool-result.d.ts +10 -0
  96. package/dist/mcp/tool-result.js +33 -0
  97. package/dist/mcp/tools/jira-context.tool.d.ts +3 -0
  98. package/dist/mcp/tools/jira-context.tool.js +25 -0
  99. package/dist/mcp/tools/jira-full.tool.d.ts +3 -0
  100. package/dist/mcp/tools/jira-full.tool.js +27 -0
  101. package/dist/mcp/tools/jira-search.tool.d.ts +3 -0
  102. package/dist/mcp/tools/jira-search.tool.js +31 -0
  103. package/dist/policy/completeness-policy.d.ts +19 -0
  104. package/dist/policy/completeness-policy.js +34 -0
  105. package/dist/policy/consistency-policy.d.ts +13 -0
  106. package/dist/policy/consistency-policy.js +6 -0
  107. package/dist/policy/field-policy.d.ts +11 -0
  108. package/dist/policy/field-policy.js +26 -0
  109. package/dist/policy/output-budget-policy.d.ts +23 -0
  110. package/dist/policy/output-budget-policy.js +96 -0
  111. package/dist/policy/pagination-policy.d.ts +15 -0
  112. package/dist/policy/pagination-policy.js +13 -0
  113. package/dist/ports/cache.port.d.ts +5 -0
  114. package/dist/ports/cache.port.js +1 -0
  115. package/dist/ports/credentials.port.d.ts +27 -0
  116. package/dist/ports/credentials.port.js +1 -0
  117. package/dist/ports/jira-read.port.d.ts +73 -0
  118. package/dist/ports/jira-read.port.js +1 -0
  119. package/dist/ports/jira-write.port.d.ts +12 -0
  120. package/dist/ports/jira-write.port.js +1 -0
  121. package/dist/ports/telemetry.port.d.ts +13 -0
  122. package/dist/ports/telemetry.port.js +1 -0
  123. package/package.json +69 -0
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * `.jira-agent/project.yaml` - per-project policy only.
4
+ * Credentials are never stored here; they come from the CredentialPort.
5
+ */
6
+ export declare const ProjectConfigSchema: z.ZodObject<{
7
+ version: z.ZodDefault<z.ZodLiteral<1>>;
8
+ project: z.ZodPrefault<z.ZodObject<{
9
+ key: z.ZodDefault<z.ZodString>;
10
+ }, z.core.$strip>>;
11
+ search: z.ZodPrefault<z.ZodObject<{
12
+ pageSize: z.ZodDefault<z.ZodNumber>;
13
+ maxPages: z.ZodDefault<z.ZodNumber>;
14
+ }, z.core.$strip>>;
15
+ fields: z.ZodPrefault<z.ZodObject<{
16
+ lite: z.ZodDefault<z.ZodArray<z.ZodString>>;
17
+ context: z.ZodDefault<z.ZodArray<z.ZodString>>;
18
+ }, z.core.$strip>>;
19
+ customFields: z.ZodDefault<z.ZodArray<z.ZodObject<{
20
+ id: z.ZodString;
21
+ name: z.ZodString;
22
+ }, z.core.$strip>>>;
23
+ output: z.ZodPrefault<z.ZodObject<{
24
+ searchTokens: z.ZodDefault<z.ZodNumber>;
25
+ contextTokens: z.ZodDefault<z.ZodNumber>;
26
+ fullTokens: z.ZodDefault<z.ZodNumber>;
27
+ }, z.core.$strip>>;
28
+ policy: z.ZodPrefault<z.ZodObject<{
29
+ contextRequiredFor: z.ZodDefault<z.ZodArray<z.ZodString>>;
30
+ fullRequiredFor: z.ZodDefault<z.ZodArray<z.ZodString>>;
31
+ }, z.core.$strip>>;
32
+ telemetry: z.ZodPrefault<z.ZodObject<{
33
+ enabled: z.ZodDefault<z.ZodBoolean>;
34
+ }, z.core.$strip>>;
35
+ }, z.core.$strip>;
36
+ export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * `.jira-agent/project.yaml` - per-project policy only.
4
+ * Credentials are never stored here; they come from the CredentialPort.
5
+ */
6
+ export const ProjectConfigSchema = z.object({
7
+ version: z.literal(1).default(1),
8
+ /**
9
+ * Empty key is allowed so `jam serve` still boots without a project.yaml;
10
+ * `jam doctor` is what flags it as unconfigured.
11
+ */
12
+ project: z
13
+ .object({
14
+ key: z.string().default(""),
15
+ })
16
+ .prefault({}),
17
+ search: z
18
+ .object({
19
+ /** Page size sent to Jira. Not the total result count. */
20
+ pageSize: z.number().int().min(1).max(100).default(50),
21
+ /** Safety stop for `scope: "complete"`. Hitting it is reported, never silent. */
22
+ maxPages: z.number().int().min(1).max(200).default(20),
23
+ })
24
+ .prefault({}),
25
+ fields: z
26
+ .object({
27
+ lite: z.array(z.string()).default([
28
+ "summary",
29
+ "status",
30
+ "assignee",
31
+ "priority",
32
+ "updated",
33
+ "labels",
34
+ "components",
35
+ ]),
36
+ context: z
37
+ .array(z.string())
38
+ .default(["parent", "subtasks", "issuelinks", "issuetype"]),
39
+ })
40
+ .prefault({}),
41
+ /**
42
+ * Whitelisted project-specific custom fields, surfaced at CONTEXT level and up.
43
+ * `id` is the Jira field id (customfield_10011); `name` is what the agent sees.
44
+ */
45
+ customFields: z
46
+ .array(z.object({
47
+ id: z.string().regex(/^customfield_\d+$/),
48
+ name: z.string().min(1),
49
+ }))
50
+ .default([]),
51
+ output: z
52
+ .object({
53
+ /** Rough token ceilings per level. Enforced by OutputBudgetPolicy. */
54
+ searchTokens: z.number().int().min(200).default(2000),
55
+ contextTokens: z.number().int().min(200).default(5000),
56
+ fullTokens: z.number().int().min(200).default(8000),
57
+ })
58
+ .prefault({}),
59
+ policy: z
60
+ .object({
61
+ contextRequiredFor: z
62
+ .array(z.string())
63
+ .default(["readiness", "dependency", "blocker", "priority"]),
64
+ fullRequiredFor: z
65
+ .array(z.string())
66
+ .default(["agreement", "contract", "approval", "closure"]),
67
+ })
68
+ .prefault({}),
69
+ telemetry: z
70
+ .object({
71
+ enabled: z.boolean().default(true),
72
+ })
73
+ .prefault({}),
74
+ });
package/dist/deps.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ import type { BootstrapSource } from "./bootstrap/project-config-bootstrapper.js";
2
+ import type { GitRemoteFn } from "./bootstrap/workspace-identity.js";
3
+ import type { ProjectConfig } from "./config/schema.js";
4
+ import type { CachePort } from "./ports/cache.port.js";
5
+ import type { CredentialPort } from "./ports/credentials.port.js";
6
+ import type { JiraReadPort } from "./ports/jira-read.port.js";
7
+ import type { TelemetryPort } from "./ports/telemetry.port.js";
8
+ /** Everything the application layer is allowed to reach for. */
9
+ export type JamDeps = {
10
+ config: ProjectConfig;
11
+ configPath?: string;
12
+ /** Where the project key came from when no config file supplied one. */
13
+ keySource?: BootstrapSource;
14
+ jira: JiraReadPort;
15
+ cache: CachePort;
16
+ telemetry: TelemetryPort;
17
+ credentials: CredentialPort;
18
+ };
19
+ export type BuildDepsOptions = {
20
+ cwd?: string;
21
+ /** Injected by tests to bypass the real REST adapter. */
22
+ jira?: JiraReadPort;
23
+ /** Injected by tests to bypass the real process/registry credential lookup. */
24
+ credentials?: CredentialPort;
25
+ /**
26
+ * Whether an explicit key (flag, env, personal binding, preset) may stand in
27
+ * when the project has no config file, and whether its absence is fatal.
28
+ * Nothing is written either way - see resolveProjectConfig.
29
+ */
30
+ keyFallback?: "required" | "optional";
31
+ /** `--project` override, passed through from `jam setup`. */
32
+ explicitKey?: string;
33
+ /** Injected by tests so a decision never depends on the machine's environment. */
34
+ env?: NodeJS.ProcessEnv;
35
+ /** Injected by tests so a decision never reads the developer's own presets. */
36
+ presetsPath?: string;
37
+ /** Injected by tests to isolate `~/.jam`. */
38
+ home?: string;
39
+ /** Injected by tests so identity never depends on the checkout under test. */
40
+ git?: GitRemoteFn;
41
+ };
42
+ /**
43
+ * Single composition root. `jam serve`, `jam doctor` and `jam setup` all wire
44
+ * through here, so a doctor pass actually proves the server's configuration.
45
+ *
46
+ * Credentials come from `CompositeCredentialProvider`: the current process's
47
+ * own environment first, then the Windows User environment as a fallback for
48
+ * shells that predate a `setx`.
49
+ */
50
+ export declare function buildDeps(options?: BuildDepsOptions): Promise<JamDeps>;
package/dist/deps.js ADDED
@@ -0,0 +1,39 @@
1
+ import { NoopCache } from "./adapters/cache/noop-cache.js";
2
+ import { CompositeCredentialProvider } from "./adapters/credentials/composite.js";
3
+ import { ConsoleTelemetry } from "./adapters/telemetry/console-telemetry.js";
4
+ import { resolveProjectConfig } from "./bootstrap/project-config-resolver.js";
5
+ /**
6
+ * Single composition root. `jam serve`, `jam doctor` and `jam setup` all wire
7
+ * through here, so a doctor pass actually proves the server's configuration.
8
+ *
9
+ * Credentials come from `CompositeCredentialProvider`: the current process's
10
+ * own environment first, then the Windows User environment as a fallback for
11
+ * shells that predate a `setx`.
12
+ */
13
+ export async function buildDeps(options = {}) {
14
+ const resolved = resolveProjectConfig({
15
+ cwd: options.cwd,
16
+ ...(options.keyFallback ? { keyFallback: options.keyFallback } : {}),
17
+ explicitKey: options.explicitKey,
18
+ ...(options.env ? { env: options.env } : {}),
19
+ ...(options.presetsPath ? { presetsPath: options.presetsPath } : {}),
20
+ ...(options.home ? { home: options.home } : {}),
21
+ ...(options.git ? { git: options.git } : {}),
22
+ });
23
+ const credentials = options.credentials ?? new CompositeCredentialProvider();
24
+ const telemetry = new ConsoleTelemetry(resolved.config.telemetry.enabled);
25
+ let jira = options.jira;
26
+ if (!jira) {
27
+ const { JiraCloudReadAdapter } = await import("./adapters/jira-cloud/jira-read.adapter.js");
28
+ jira = new JiraCloudReadAdapter(credentials, resolved.config);
29
+ }
30
+ return {
31
+ config: resolved.config,
32
+ configPath: resolved.configPath,
33
+ keySource: resolved.keySource,
34
+ jira,
35
+ cache: new NoopCache(),
36
+ telemetry,
37
+ credentials,
38
+ };
39
+ }
@@ -0,0 +1,63 @@
1
+ export type ContextLevel = "search" | "context" | "full";
2
+ export type IncompleteReason = "OUTPUT_BUDGET" | "PERMISSION" | "PARTIAL_API_RESPONSE" | "UNKNOWN";
3
+ /**
4
+ * What JAM did not look at.
5
+ *
6
+ * Stable codes, never prose: a consumer has to branch on these, and a
7
+ * sentence that gets reworded silently breaks whoever was matching on it.
8
+ * Every JAM result carries all of them today, because JAM reads Jira and
9
+ * nothing else - they exist so that fact is in the payload rather than only
10
+ * in a tool description nobody re-reads.
11
+ */
12
+ export type EvidenceLimitation = "REPOSITORY_NOT_EVALUATED" | "EXTERNAL_SOURCES_NOT_EVALUATED" | "NON_JIRA_DEPENDENCIES_NOT_EVALUATED";
13
+ /**
14
+ * Attached to every tool result so the agent can tell a full picture from a
15
+ * partial one. Silent truncation is prohibited: anything dropped must show up
16
+ * here with `complete: false` and a reason.
17
+ *
18
+ * `complete` is about retrieval and nothing else: JAM finished the Jira read
19
+ * it was asked for with no known loss. It is not a claim that Jira holds the
20
+ * whole story, that the repository agrees, or that the work can start - which
21
+ * is what `evidenceScope` and `limitations` are here to say out loud.
22
+ */
23
+ export type CompletenessMeta = {
24
+ level: ContextLevel;
25
+ complete: boolean;
26
+ /** Where this came from. One system today, stated rather than assumed. */
27
+ source: "jira";
28
+ /**
29
+ * Whether Jira itself answered this read, or a cache did. On a cache hit
30
+ * `fetchedAt` keeps the time Jira was read - it must never be rewritten to
31
+ * the time the cache answered, or a stale record would look fresh.
32
+ */
33
+ provenance: "live" | "cache";
34
+ /** What kind of evidence this is. Jira records - not project reality. */
35
+ evidenceScope: "jira-records-only";
36
+ limitations: EvidenceLimitation[];
37
+ /** When this snapshot was read from Jira. */
38
+ fetchedAt: string;
39
+ pagesFetched?: number;
40
+ fieldsLoaded?: string[];
41
+ commentsComplete?: boolean;
42
+ linksComplete?: boolean;
43
+ reason?: IncompleteReason;
44
+ /** Which part of the payload was dropped or could not be read. */
45
+ overflow?: string[];
46
+ /** Issue keys the request asked for but could not be returned. */
47
+ missingKeys?: string[];
48
+ notes?: string[];
49
+ };
50
+ /**
51
+ * The evidence boundary every JAM result carries.
52
+ *
53
+ * One constant rather than three copies: the day JAM reads something that is
54
+ * not Jira, or serves a read from cache, exactly one of these has to change
55
+ * and the three tools cannot drift apart in the meantime.
56
+ */
57
+ export declare const JIRA_EVIDENCE: {
58
+ readonly source: "jira";
59
+ readonly provenance: "live";
60
+ readonly evidenceScope: "jira-records-only";
61
+ readonly limitations: ["REPOSITORY_NOT_EVALUATED", "EXTERNAL_SOURCES_NOT_EVALUATED", "NON_JIRA_DEPENDENCIES_NOT_EVALUATED"];
62
+ };
63
+ export declare function nowIso(): string;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The evidence boundary every JAM result carries.
3
+ *
4
+ * One constant rather than three copies: the day JAM reads something that is
5
+ * not Jira, or serves a read from cache, exactly one of these has to change
6
+ * and the three tools cannot drift apart in the meantime.
7
+ */
8
+ export const JIRA_EVIDENCE = {
9
+ source: "jira",
10
+ provenance: "live",
11
+ evidenceScope: "jira-records-only",
12
+ limitations: [
13
+ "REPOSITORY_NOT_EVALUATED",
14
+ "EXTERNAL_SOURCES_NOT_EVALUATED",
15
+ "NON_JIRA_DEPENDENCIES_NOT_EVALUATED",
16
+ ],
17
+ };
18
+ export function nowIso() {
19
+ return new Date().toISOString();
20
+ }
@@ -0,0 +1,38 @@
1
+ import type { IssueLink, IssueRef, IssueSummary } from "./issue.js";
2
+ /** CONTEXT level: enough to judge readiness, blockers, dependencies, priority. */
3
+ export type IssueContext = IssueSummary & {
4
+ issueType?: string;
5
+ parent?: IssueRef;
6
+ subtasks: IssueRef[];
7
+ links: IssueLink[];
8
+ customFields: Record<string, unknown>;
9
+ };
10
+ export type NormalizedComment = {
11
+ id: string;
12
+ author?: string;
13
+ created: string;
14
+ updated?: string;
15
+ body: string;
16
+ };
17
+ export type RelevantHistory = {
18
+ created: string;
19
+ author?: string;
20
+ field: string;
21
+ from?: string;
22
+ to?: string;
23
+ };
24
+ /** FULL level: the Jira record for agreement, contract, approval, closure. */
25
+ export type FullIssueContext = IssueContext & {
26
+ description?: string;
27
+ comments: NormalizedComment[];
28
+ history?: RelevantHistory[];
29
+ /**
30
+ * The most recent timestamp on any comment retrieved, edits included.
31
+ *
32
+ * A raw fact, not a verdict: how old is too old differs per team, so JAM
33
+ * reports when the thread last moved and leaves "stale" to the caller.
34
+ * Costs no extra Jira call - it is computed from comments already fetched,
35
+ * and is therefore as complete as `commentsComplete` says they are.
36
+ */
37
+ latestCommentAt?: string;
38
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Normalized error model.
3
+ *
4
+ * Jira raw errors never reach the agent. Everything crossing the tool boundary
5
+ * is mapped onto one of these codes so the agent (and `jam doctor`) can reason
6
+ * about failures without parsing vendor-specific payloads.
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"];
9
+ export type JamErrorCode = (typeof JAM_ERROR_CODES)[number];
10
+ export type JamErrorPayload = {
11
+ error: {
12
+ code: JamErrorCode;
13
+ message: string;
14
+ details?: Record<string, unknown>;
15
+ };
16
+ };
17
+ export declare class JamError extends Error {
18
+ readonly code: JamErrorCode;
19
+ readonly details?: Record<string, unknown>;
20
+ constructor(code: JamErrorCode, message: string, details?: Record<string, unknown>);
21
+ toPayload(): JamErrorPayload;
22
+ }
23
+ /**
24
+ * Coerce anything thrown into a JamError. Unknown failures become
25
+ * JIRA_UNAVAILABLE rather than leaking a raw stack trace to the agent.
26
+ */
27
+ export declare function toJamError(err: unknown): JamError;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Normalized error model.
3
+ *
4
+ * Jira raw errors never reach the agent. Everything crossing the tool boundary
5
+ * is mapped onto one of these codes so the agent (and `jam doctor`) can reason
6
+ * about failures without parsing vendor-specific payloads.
7
+ */
8
+ export const JAM_ERROR_CODES = [
9
+ "JIRA_AUTH_FAILED",
10
+ "JIRA_PERMISSION_DENIED",
11
+ "JQL_INVALID",
12
+ "ISSUE_NOT_FOUND",
13
+ "RATE_LIMITED",
14
+ "CONTEXT_TOO_LARGE",
15
+ "PARTIAL_RESULT",
16
+ "CONFIG_INVALID",
17
+ "JIRA_UNAVAILABLE",
18
+ "JAM_SETUP_REQUIRED",
19
+ "JAM_BINDINGS_UNREADABLE",
20
+ ];
21
+ export class JamError extends Error {
22
+ code;
23
+ details;
24
+ constructor(code, message, details) {
25
+ super(message);
26
+ this.name = "JamError";
27
+ this.code = code;
28
+ this.details = details;
29
+ }
30
+ toPayload() {
31
+ return {
32
+ error: {
33
+ code: this.code,
34
+ message: this.message,
35
+ ...(this.details ? { details: this.details } : {}),
36
+ },
37
+ };
38
+ }
39
+ }
40
+ /**
41
+ * Coerce anything thrown into a JamError. Unknown failures become
42
+ * JIRA_UNAVAILABLE rather than leaking a raw stack trace to the agent.
43
+ */
44
+ export function toJamError(err) {
45
+ if (err instanceof JamError)
46
+ return err;
47
+ const message = err instanceof Error ? err.message : String(err);
48
+ return new JamError("JIRA_UNAVAILABLE", message);
49
+ }
@@ -0,0 +1,35 @@
1
+ export type IssueRef = {
2
+ key: string;
3
+ summary?: string;
4
+ status?: string;
5
+ };
6
+ /**
7
+ * SEARCH level. Deliberately excludes description/comments/attachments/changelog
8
+ * so discovery stays cheap.
9
+ */
10
+ export type IssueSummary = {
11
+ key: string;
12
+ summary: string;
13
+ status: string;
14
+ assignee?: string;
15
+ priority?: string;
16
+ updated: string;
17
+ labels: string[];
18
+ components: string[];
19
+ };
20
+ export type LinkDirection = "outward" | "inward";
21
+ export type IssueLink = {
22
+ /** Human-readable relationship as Jira words it, e.g. "blocks", "is blocked by". */
23
+ type: string;
24
+ direction: LinkDirection;
25
+ issue: IssueRef;
26
+ /**
27
+ * True when Jira's issue-link semantics put this issue on the blocked side.
28
+ *
29
+ * A reading of the link, not a verdict on the work: it is derived from how
30
+ * Jira words the relationship, so `true` is not a finding that work cannot
31
+ * start, and `false` is not a finding that nothing blocks it. Whatever is
32
+ * only written in a description, an MR or a spec is not represented here.
33
+ */
34
+ blocksThisIssue: boolean;
35
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { runJamCommand } from "./cli-entry.js";
3
+ import { toJamError } from "./domain/errors.js";
4
+ runJamCommand(process.argv.slice(2))
5
+ .then((code) => {
6
+ // Setting exitCode and letting Node exit naturally (rather than forcing
7
+ // process.exit()) avoids a libuv assertion crash observed on Windows when
8
+ // this process has mixed spawnSync (reg.exe/where) with async fetch calls -
9
+ // a forced exit can race a handle that is still closing.
10
+ if (code >= 0)
11
+ process.exitCode = code;
12
+ })
13
+ .catch((err) => {
14
+ const jamError = toJamError(err);
15
+ process.stderr.write(`[jam] ${jamError.code}: ${jamError.message}\n`);
16
+ process.exitCode = 1;
17
+ });
@@ -0,0 +1,9 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { JamDeps } from "../deps.js";
3
+ export declare const SERVER_NAME = "jam";
4
+ /**
5
+ * The external contract: exactly three read tools, stable from the first
6
+ * release. Internal changes (cache, Rovo, remote transport) must not add or
7
+ * rename anything here.
8
+ */
9
+ export declare function createServer(deps: JamDeps): McpServer;
@@ -0,0 +1,32 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { createRequire } from "node:module";
3
+ import { registerJiraContext } from "./tools/jira-context.tool.js";
4
+ import { registerJiraFull } from "./tools/jira-full.tool.js";
5
+ import { registerJiraSearch } from "./tools/jira-search.tool.js";
6
+ const require = createRequire(import.meta.url);
7
+ const pkg = require("../../package.json");
8
+ export const SERVER_NAME = "jam";
9
+ /**
10
+ * The external contract: exactly three read tools, stable from the first
11
+ * release. Internal changes (cache, Rovo, remote transport) must not add or
12
+ * rename anything here.
13
+ */
14
+ export function createServer(deps) {
15
+ const server = new McpServer({ name: SERVER_NAME, version: pkg.version ?? "0.0.0" }, {
16
+ instructions: [
17
+ "JAM (Jira Agent MCP) is the default path for reading Jira.",
18
+ "Pick the tool by what the answer will be used for:",
19
+ "- listing / discovery / current status -> jira_search",
20
+ "- readiness, blockers, dependencies, priority -> jira_context",
21
+ "- agreement, contract, approval, closure -> jira_full",
22
+ "Never treat a jira_search result as complete issue context.",
23
+ "Every result carries a `meta` block; if meta.complete is false the answer is partial and must be reported as such.",
24
+ "meta.complete describes JAM's retrieval, not the project: it means the Jira read finished with no known loss, never that Jira holds the whole story.",
25
+ "meta.evidenceScope and meta.limitations name what was not evaluated - the repository and every external source among them. Judge Jira evidence from these results; judge execution reality elsewhere.",
26
+ ].join("\n"),
27
+ });
28
+ registerJiraSearch(server, deps);
29
+ registerJiraContext(server, deps);
30
+ registerJiraFull(server, deps);
31
+ return server;
32
+ }
@@ -0,0 +1,10 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { TelemetryPort } from "../ports/telemetry.port.js";
3
+ /** Compact JSON - indentation is pure token cost on this path. */
4
+ export declare function ok(payload: unknown): CallToolResult;
5
+ export declare function fail(err: unknown): CallToolResult;
6
+ /**
7
+ * Every tool goes through here so a thrown Jira error becomes a normalized code
8
+ * instead of a stack trace, and failures still show up in telemetry.
9
+ */
10
+ export declare function runTool(tool: string, telemetry: TelemetryPort, fn: () => Promise<unknown>): Promise<CallToolResult>;
@@ -0,0 +1,33 @@
1
+ import { toJamError } from "../domain/errors.js";
2
+ /** Compact JSON - indentation is pure token cost on this path. */
3
+ export function ok(payload) {
4
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
5
+ }
6
+ export function fail(err) {
7
+ return {
8
+ isError: true,
9
+ content: [{ type: "text", text: JSON.stringify(toJamError(err).toPayload()) }],
10
+ };
11
+ }
12
+ /**
13
+ * Every tool goes through here so a thrown Jira error becomes a normalized code
14
+ * instead of a stack trace, and failures still show up in telemetry.
15
+ */
16
+ export async function runTool(tool, telemetry, fn) {
17
+ const started = performance.now();
18
+ try {
19
+ return ok(await fn());
20
+ }
21
+ catch (err) {
22
+ const jamError = toJamError(err);
23
+ telemetry.recordTool({
24
+ tool,
25
+ durationMs: performance.now() - started,
26
+ jiraRequests: 0,
27
+ issues: 0,
28
+ responseBytes: 0,
29
+ errorCode: jamError.code,
30
+ });
31
+ return fail(jamError);
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { JamDeps } from "../../deps.js";
3
+ export declare function registerJiraContext(server: McpServer, deps: JamDeps): void;
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { getIssueContext } from "../../application/get-issue-context.js";
3
+ import { runTool } from "../tool-result.js";
4
+ const DESCRIPTION = `Get the dependency and readiness picture for one or more Jira issues.
5
+
6
+ Use for: "can I start this", "what is blocking it", "what should I do first", parent/subtask structure, cross-team dependencies, priority ordering.
7
+
8
+ Returns everything jira_search returns plus issue type, parent, subtasks, issue links (with blocksThisIssue flagged) and the project's whitelisted custom fields. It does NOT return the comment thread - if the question is whether something was agreed, approved, or finished, use jira_full instead.
9
+
10
+ This is the Jira-recorded evidence relevant to readiness, blockers, dependencies and priority - not a readiness verdict. blocksThisIssue reports how Jira words a link; an empty links array means Jira holds no visible link for you, not that nothing blocks the work. Repository and external sources are not evaluated.
11
+
12
+ Pass every key you care about in one call; they are fetched in a single batched round trip. Check meta.complete and meta.missingKeys before drawing conclusions.`;
13
+ export function registerJiraContext(server, deps) {
14
+ server.registerTool("jira_context", {
15
+ title: "Jira issue context (dependencies, blockers, readiness)",
16
+ description: DESCRIPTION,
17
+ inputSchema: {
18
+ issueKeys: z
19
+ .array(z.string().min(1))
20
+ .min(1)
21
+ .describe('Issue keys, e.g. ["PROJECT-97", "PROJECT-101"]. Batch them in one call.'),
22
+ },
23
+ annotations: { readOnlyHint: true, openWorldHint: true },
24
+ }, async (args) => runTool("jira_context", deps.telemetry, () => getIssueContext(deps, { issueKeys: args.issueKeys })));
25
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { JamDeps } from "../../deps.js";
3
+ export declare function registerJiraFull(server: McpServer, deps: JamDeps): void;
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { getFullIssueContext } from "../../application/get-full-issue-context.js";
3
+ import { runTool } from "../tool-result.js";
4
+ const DESCRIPTION = `Get the complete record for one or more Jira issues, including the description and the full comment thread.
5
+
6
+ Use for final judgements: was this agreed, is the contract settled, was it approved, can it be closed, what did the other team actually answer, what does this issue mean right now.
7
+
8
+ Returns everything jira_context returns plus description and every comment (normalized to plain text). This is the most expensive tool - prefer jira_search for listing and jira_context for readiness, and reach for this one when the answer must not be wrong.
9
+
10
+ Ask for as few keys as possible: with several issues at once the output budget may drop the oldest comments. Always check meta.commentsComplete and meta.complete - if either is false, the thread you are reading is partial and a "yes, it is agreed" answer is not supported.
11
+
12
+ This is the Jira-recorded evidence relevant to agreement, contract, approval and closure. Repository and external sources are not evaluated.
13
+
14
+ Absence of evidence in Jira is not evidence of absence. A complete read with no supporting comments proves only what Jira holds. If the issue references an external canonical source (an MR/PR, a spec or contract document, Confluence, another issue), do not conclude "not agreed", "not approved" or "cannot start": check that source if you can reach it, and otherwise report that Jira alone cannot settle the question and name the source that must be checked. Issues that reference no external source do not warrant an open-ended search.`;
15
+ export function registerJiraFull(server, deps) {
16
+ server.registerTool("jira_full", {
17
+ title: "Full Jira issue record (description + comments)",
18
+ description: DESCRIPTION,
19
+ inputSchema: {
20
+ issueKeys: z
21
+ .array(z.string().min(1))
22
+ .min(1)
23
+ .describe('Issue keys, e.g. ["PROJECT-97"]. Keep the list short - the full record is large.'),
24
+ },
25
+ annotations: { readOnlyHint: true, openWorldHint: true },
26
+ }, async (args) => runTool("jira_full", deps.telemetry, () => getFullIssueContext(deps, { issueKeys: args.issueKeys })));
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { JamDeps } from "../../deps.js";
3
+ export declare function registerJiraSearch(server: McpServer, deps: JamDeps): void;