@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.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/adapters/cache/noop-cache.d.ts +7 -0
- package/dist/adapters/cache/noop-cache.js +12 -0
- package/dist/adapters/credentials/composite.d.ts +30 -0
- package/dist/adapters/credentials/composite.js +79 -0
- package/dist/adapters/credentials/process-env.d.ts +13 -0
- package/dist/adapters/credentials/process-env.js +17 -0
- package/dist/adapters/credentials/secret-store.d.ts +69 -0
- package/dist/adapters/credentials/secret-store.js +259 -0
- package/dist/adapters/credentials/windows-user-env.d.ts +20 -0
- package/dist/adapters/credentials/windows-user-env.js +51 -0
- package/dist/adapters/jira-cloud/adf-to-text.d.ts +12 -0
- package/dist/adapters/jira-cloud/adf-to-text.js +151 -0
- package/dist/adapters/jira-cloud/jira-client.d.ts +32 -0
- package/dist/adapters/jira-cloud/jira-client.js +137 -0
- package/dist/adapters/jira-cloud/jira-read.adapter.d.ts +13 -0
- package/dist/adapters/jira-cloud/jira-read.adapter.js +97 -0
- package/dist/adapters/jira-cloud/jira-write.adapter.d.ts +12 -0
- package/dist/adapters/jira-cloud/jira-write.adapter.js +17 -0
- package/dist/adapters/jira-cloud/mapper.d.ts +34 -0
- package/dist/adapters/jira-cloud/mapper.js +143 -0
- package/dist/adapters/telemetry/console-telemetry.d.ts +13 -0
- package/dist/adapters/telemetry/console-telemetry.js +31 -0
- package/dist/application/get-full-issue-context.d.ts +18 -0
- package/dist/application/get-full-issue-context.js +120 -0
- package/dist/application/get-issue-context.d.ts +19 -0
- package/dist/application/get-issue-context.js +79 -0
- package/dist/application/search-issues.d.ts +22 -0
- package/dist/application/search-issues.js +93 -0
- package/dist/bootstrap/boot-health-gate.d.ts +25 -0
- package/dist/bootstrap/boot-health-gate.js +137 -0
- package/dist/bootstrap/bootstrap-orchestrator.d.ts +15 -0
- package/dist/bootstrap/bootstrap-orchestrator.js +14 -0
- package/dist/bootstrap/host-mcp.d.ts +54 -0
- package/dist/bootstrap/host-mcp.js +98 -0
- package/dist/bootstrap/jira-projects.d.ts +16 -0
- package/dist/bootstrap/jira-projects.js +24 -0
- package/dist/bootstrap/mcp-config-merger.d.ts +58 -0
- package/dist/bootstrap/mcp-config-merger.js +119 -0
- package/dist/bootstrap/migration-target.d.ts +60 -0
- package/dist/bootstrap/migration-target.js +80 -0
- package/dist/bootstrap/project-bindings.d.ts +48 -0
- package/dist/bootstrap/project-bindings.js +112 -0
- package/dist/bootstrap/project-config-bootstrapper.d.ts +37 -0
- package/dist/bootstrap/project-config-bootstrapper.js +69 -0
- package/dist/bootstrap/project-config-resolver.d.ts +53 -0
- package/dist/bootstrap/project-config-resolver.js +59 -0
- package/dist/bootstrap/project-root-resolver.d.ts +22 -0
- package/dist/bootstrap/project-root-resolver.js +43 -0
- package/dist/bootstrap/setup-apply.d.ts +31 -0
- package/dist/bootstrap/setup-apply.js +61 -0
- package/dist/bootstrap/setup-plan.d.ts +102 -0
- package/dist/bootstrap/setup-plan.js +224 -0
- package/dist/bootstrap/setup-state.d.ts +73 -0
- package/dist/bootstrap/setup-state.js +89 -0
- package/dist/bootstrap/workspace-identity.d.ts +52 -0
- package/dist/bootstrap/workspace-identity.js +114 -0
- package/dist/cli/agent-api.d.ts +63 -0
- package/dist/cli/agent-api.js +149 -0
- package/dist/cli/auth.d.ts +39 -0
- package/dist/cli/auth.js +201 -0
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +31 -0
- package/dist/cli/runtime.d.ts +16 -0
- package/dist/cli/runtime.js +97 -0
- package/dist/cli/serve.d.ts +7 -0
- package/dist/cli/serve.js +23 -0
- package/dist/cli/setup-wizard.d.ts +37 -0
- package/dist/cli/setup-wizard.js +338 -0
- package/dist/cli/setup.d.ts +26 -0
- package/dist/cli/setup.js +214 -0
- package/dist/cli/ui.d.ts +118 -0
- package/dist/cli/ui.js +338 -0
- package/dist/cli-entry.d.ts +7 -0
- package/dist/cli-entry.js +124 -0
- package/dist/config/load-config.d.ts +18 -0
- package/dist/config/load-config.js +48 -0
- package/dist/config/schema.d.ts +36 -0
- package/dist/config/schema.js +74 -0
- package/dist/deps.d.ts +50 -0
- package/dist/deps.js +39 -0
- package/dist/domain/completeness.d.ts +63 -0
- package/dist/domain/completeness.js +20 -0
- package/dist/domain/context.d.ts +38 -0
- package/dist/domain/context.js +1 -0
- package/dist/domain/errors.d.ts +27 -0
- package/dist/domain/errors.js +49 -0
- package/dist/domain/issue.d.ts +35 -0
- package/dist/domain/issue.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/mcp/create-server.d.ts +9 -0
- package/dist/mcp/create-server.js +32 -0
- package/dist/mcp/tool-result.d.ts +10 -0
- package/dist/mcp/tool-result.js +33 -0
- package/dist/mcp/tools/jira-context.tool.d.ts +3 -0
- package/dist/mcp/tools/jira-context.tool.js +25 -0
- package/dist/mcp/tools/jira-full.tool.d.ts +3 -0
- package/dist/mcp/tools/jira-full.tool.js +27 -0
- package/dist/mcp/tools/jira-search.tool.d.ts +3 -0
- package/dist/mcp/tools/jira-search.tool.js +31 -0
- package/dist/policy/completeness-policy.d.ts +19 -0
- package/dist/policy/completeness-policy.js +34 -0
- package/dist/policy/consistency-policy.d.ts +13 -0
- package/dist/policy/consistency-policy.js +6 -0
- package/dist/policy/field-policy.d.ts +11 -0
- package/dist/policy/field-policy.js +26 -0
- package/dist/policy/output-budget-policy.d.ts +23 -0
- package/dist/policy/output-budget-policy.js +96 -0
- package/dist/policy/pagination-policy.d.ts +15 -0
- package/dist/policy/pagination-policy.js +13 -0
- package/dist/ports/cache.port.d.ts +5 -0
- package/dist/ports/cache.port.js +1 -0
- package/dist/ports/credentials.port.d.ts +27 -0
- package/dist/ports/credentials.port.js +1 -0
- package/dist/ports/jira-read.port.d.ts +73 -0
- package/dist/ports/jira-read.port.js +1 -0
- package/dist/ports/jira-write.port.d.ts +12 -0
- package/dist/ports/jira-write.port.js +1 -0
- package/dist/ports/telemetry.port.d.ts +13 -0
- package/dist/ports/telemetry.port.js +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { searchIssues } from "../../application/search-issues.js";
|
|
3
|
+
import { runTool } from "../tool-result.js";
|
|
4
|
+
const DESCRIPTION = `Find Jira issues by JQL and get a lightweight list back.
|
|
5
|
+
|
|
6
|
+
Use for: discovery, listing, "what is open", "what is assigned to me", recent changes, picking candidate issues.
|
|
7
|
+
|
|
8
|
+
Returns key, summary, status, assignee, priority, updated, labels and components only. It deliberately does NOT return description, comments, attachments or links - that keeps listing cheap.
|
|
9
|
+
|
|
10
|
+
Because of that, a jira_search result is NOT complete issue context. Never conclude from it that something is agreed, approved, unblocked, or done. Follow up with jira_context (readiness, blockers, dependencies, priority) or jira_full (agreement, contract, approval, closure).
|
|
11
|
+
|
|
12
|
+
Repository and external sources are not evaluated.
|
|
13
|
+
|
|
14
|
+
scope="preview" (default) returns the first page for interactive exploration. scope="complete" walks every page - use it whenever the answer depends on the total count or on seeing every match. Check meta.complete before treating the list as exhaustive.`;
|
|
15
|
+
export function registerJiraSearch(server, deps) {
|
|
16
|
+
server.registerTool("jira_search", {
|
|
17
|
+
title: "Search Jira issues (lightweight list)",
|
|
18
|
+
description: DESCRIPTION,
|
|
19
|
+
inputSchema: {
|
|
20
|
+
jql: z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1)
|
|
23
|
+
.describe('JQL query, e.g. \'project = PROJECT AND statusCategory != Done ORDER BY updated DESC\'.'),
|
|
24
|
+
scope: z
|
|
25
|
+
.enum(["preview", "complete"])
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('"preview" (default) = first page only. "complete" = every page, for exhaustive enumeration or counting.'),
|
|
28
|
+
},
|
|
29
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
30
|
+
}, async (args) => runTool("jira_search", deps.telemetry, () => searchIssues(deps, { jql: args.jql, ...(args.scope ? { scope: args.scope } : {}) })));
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ContextLevel } from "../domain/completeness.js";
|
|
2
|
+
import type { ProjectConfig } from "../config/schema.js";
|
|
3
|
+
/**
|
|
4
|
+
* Which tool a given kind of judgement requires. There is no AI intent
|
|
5
|
+
* classifier in this release - the rule is enforced through tool descriptions
|
|
6
|
+
* and project instructions, and this table is the single place it is written down.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_CONTEXT_DECISIONS: readonly ["readiness", "dependency", "blocker", "priority"];
|
|
9
|
+
export declare const DEFAULT_FULL_DECISIONS: readonly ["agreement", "contract", "approval", "closure"];
|
|
10
|
+
export declare function minimumLevelFor(decision: string, config: ProjectConfig): ContextLevel;
|
|
11
|
+
/**
|
|
12
|
+
* Human-readable guidance embedded in each tool's description so the model
|
|
13
|
+
* picks the right level without a router.
|
|
14
|
+
*/
|
|
15
|
+
export declare const LEVEL_GUIDANCE: {
|
|
16
|
+
readonly search: "Listing and discovery only. Results are NOT complete issue context: never conclude agreement, approval, or done-ness from them.";
|
|
17
|
+
readonly context: "Readiness, blockers, dependencies and priority. Includes parent/subtasks/links but not the comment thread.";
|
|
18
|
+
readonly full: "Agreement, contract, approval and closure. Includes description and the full comment thread.";
|
|
19
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tool a given kind of judgement requires. There is no AI intent
|
|
3
|
+
* classifier in this release - the rule is enforced through tool descriptions
|
|
4
|
+
* and project instructions, and this table is the single place it is written down.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_CONTEXT_DECISIONS = [
|
|
7
|
+
"readiness",
|
|
8
|
+
"dependency",
|
|
9
|
+
"blocker",
|
|
10
|
+
"priority",
|
|
11
|
+
];
|
|
12
|
+
export const DEFAULT_FULL_DECISIONS = [
|
|
13
|
+
"agreement",
|
|
14
|
+
"contract",
|
|
15
|
+
"approval",
|
|
16
|
+
"closure",
|
|
17
|
+
];
|
|
18
|
+
export function minimumLevelFor(decision, config) {
|
|
19
|
+
const d = decision.trim().toLowerCase();
|
|
20
|
+
if (config.policy.fullRequiredFor.some((x) => x.toLowerCase() === d))
|
|
21
|
+
return "full";
|
|
22
|
+
if (config.policy.contextRequiredFor.some((x) => x.toLowerCase() === d))
|
|
23
|
+
return "context";
|
|
24
|
+
return "search";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Human-readable guidance embedded in each tool's description so the model
|
|
28
|
+
* picks the right level without a router.
|
|
29
|
+
*/
|
|
30
|
+
export const LEVEL_GUIDANCE = {
|
|
31
|
+
search: "Listing and discovery only. Results are NOT complete issue context: never conclude agreement, approval, or done-ness from them.",
|
|
32
|
+
context: "Readiness, blockers, dependencies and priority. Includes parent/subtasks/links but not the comment thread.",
|
|
33
|
+
full: "Agreement, contract, approval and closure. Includes description and the full comment thread.",
|
|
34
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-after-write rule.
|
|
3
|
+
*
|
|
4
|
+
* JAM is read-only in this release, so nothing enforces this at runtime yet.
|
|
5
|
+
* The rule is fixed here so the write adapter, when it lands, cannot quietly
|
|
6
|
+
* confirm a write with a stale JQL search result.
|
|
7
|
+
*
|
|
8
|
+
* Normal read -> Enhanced JQL search (`jira_search`)
|
|
9
|
+
* Post-write read -> direct issue GET for the affected key
|
|
10
|
+
*/
|
|
11
|
+
export type ReadMode = "search" | "direct";
|
|
12
|
+
export declare function readModeAfterWrite(): ReadMode;
|
|
13
|
+
export declare function readModeForQuery(): ReadMode;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProjectConfig } from "../config/schema.js";
|
|
2
|
+
import type { ContextLevel } from "../domain/completeness.js";
|
|
3
|
+
/**
|
|
4
|
+
* The agent never chooses Jira fields. Each context level has a fixed field set
|
|
5
|
+
* so a discovery call cannot accidentally drag description/comments into the
|
|
6
|
+
* model's context, and `fields=*` is impossible by construction.
|
|
7
|
+
*/
|
|
8
|
+
/** Never requested at SEARCH level - these are the expensive ones. */
|
|
9
|
+
export declare const HEAVY_FIELDS: readonly ["description", "comment", "attachment", "changelog"];
|
|
10
|
+
export declare function fieldsFor(level: ContextLevel, config: ProjectConfig): string[];
|
|
11
|
+
export declare function isHeavy(field: string): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent never chooses Jira fields. Each context level has a fixed field set
|
|
3
|
+
* so a discovery call cannot accidentally drag description/comments into the
|
|
4
|
+
* model's context, and `fields=*` is impossible by construction.
|
|
5
|
+
*/
|
|
6
|
+
/** Never requested at SEARCH level - these are the expensive ones. */
|
|
7
|
+
export const HEAVY_FIELDS = ["description", "comment", "attachment", "changelog"];
|
|
8
|
+
export function fieldsFor(level, config) {
|
|
9
|
+
const lite = dedupe(config.fields.lite.filter((f) => !isHeavy(f)));
|
|
10
|
+
if (level === "search")
|
|
11
|
+
return lite;
|
|
12
|
+
const contextFields = dedupe([
|
|
13
|
+
...lite,
|
|
14
|
+
...config.fields.context.filter((f) => !isHeavy(f)),
|
|
15
|
+
...config.customFields.map((cf) => cf.id),
|
|
16
|
+
]);
|
|
17
|
+
if (level === "context")
|
|
18
|
+
return contextFields;
|
|
19
|
+
return dedupe([...contextFields, "description", "comment"]);
|
|
20
|
+
}
|
|
21
|
+
export function isHeavy(field) {
|
|
22
|
+
return HEAVY_FIELDS.includes(field);
|
|
23
|
+
}
|
|
24
|
+
function dedupe(fields) {
|
|
25
|
+
return [...new Set(fields.map((f) => f.trim()).filter(Boolean))];
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FullIssueContext } from "../domain/context.js";
|
|
2
|
+
/**
|
|
3
|
+
* Output budget enforcement.
|
|
4
|
+
*
|
|
5
|
+
* The rule that matters is not "stay small" but "never lie about being small".
|
|
6
|
+
* Anything dropped here is reported through `overflow` so the caller can put it
|
|
7
|
+
* in completeness metadata; silent truncation is a release blocker.
|
|
8
|
+
*
|
|
9
|
+
* Drop order is the inverse of the design's keep-priority:
|
|
10
|
+
* core metadata > description > links/dependencies > comments > changelog
|
|
11
|
+
*/
|
|
12
|
+
/** Rough proxy for tokens. Cheap, dependency-free, tuned against real payloads later. */
|
|
13
|
+
export declare function estimateTokens(value: unknown): number;
|
|
14
|
+
export type BudgetOutcome = {
|
|
15
|
+
issues: FullIssueContext[];
|
|
16
|
+
/** Which parts were dropped, e.g. ["comments", "description"]. */
|
|
17
|
+
overflow: string[];
|
|
18
|
+
complete: boolean;
|
|
19
|
+
commentsComplete: boolean;
|
|
20
|
+
linksComplete: boolean;
|
|
21
|
+
droppedComments: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function applyOutputBudget(input: FullIssueContext[], budgetTokens: number): BudgetOutcome;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output budget enforcement.
|
|
3
|
+
*
|
|
4
|
+
* The rule that matters is not "stay small" but "never lie about being small".
|
|
5
|
+
* Anything dropped here is reported through `overflow` so the caller can put it
|
|
6
|
+
* in completeness metadata; silent truncation is a release blocker.
|
|
7
|
+
*
|
|
8
|
+
* Drop order is the inverse of the design's keep-priority:
|
|
9
|
+
* core metadata > description > links/dependencies > comments > changelog
|
|
10
|
+
*/
|
|
11
|
+
/** Rough proxy for tokens. Cheap, dependency-free, tuned against real payloads later. */
|
|
12
|
+
export function estimateTokens(value) {
|
|
13
|
+
if (value === undefined)
|
|
14
|
+
return 0;
|
|
15
|
+
return Math.ceil(JSON.stringify(value).length / 4);
|
|
16
|
+
}
|
|
17
|
+
export function applyOutputBudget(input, budgetTokens) {
|
|
18
|
+
const overflow = new Set();
|
|
19
|
+
let droppedComments = 0;
|
|
20
|
+
// Work on copies so callers keep their originals intact.
|
|
21
|
+
const issues = input.map((i) => ({ ...i, comments: [...i.comments] }));
|
|
22
|
+
const stripped = issues.map((i) => bare(i));
|
|
23
|
+
let total = estimateTokens(stripped);
|
|
24
|
+
const historyCost = issues.map((i) => estimateTokens(i.history));
|
|
25
|
+
const descriptionCost = issues.map((i) => estimateTokens(i.description));
|
|
26
|
+
const linkCost = issues.map((i) => estimateTokens(i.links) + estimateTokens(i.subtasks));
|
|
27
|
+
total +=
|
|
28
|
+
sum(historyCost) +
|
|
29
|
+
sum(descriptionCost) +
|
|
30
|
+
sum(linkCost) +
|
|
31
|
+
sum(issues.map((i) => sum(i.comments.map(estimateTokens))));
|
|
32
|
+
const fits = () => total <= budgetTokens;
|
|
33
|
+
// 1. changelog
|
|
34
|
+
if (!fits()) {
|
|
35
|
+
issues.forEach((issue, idx) => {
|
|
36
|
+
if (issue.history?.length) {
|
|
37
|
+
total -= historyCost[idx] ?? 0;
|
|
38
|
+
delete issue.history;
|
|
39
|
+
overflow.add("history");
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// 2. comments - oldest first, so the newest (decision-bearing) ones survive
|
|
44
|
+
if (!fits()) {
|
|
45
|
+
for (const issue of issues) {
|
|
46
|
+
while (issue.comments.length > 0 && !fits()) {
|
|
47
|
+
const oldest = issue.comments.shift();
|
|
48
|
+
total -= estimateTokens(oldest);
|
|
49
|
+
droppedComments++;
|
|
50
|
+
overflow.add("comments");
|
|
51
|
+
}
|
|
52
|
+
if (fits())
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// 3. links and subtasks
|
|
57
|
+
if (!fits()) {
|
|
58
|
+
issues.forEach((issue, idx) => {
|
|
59
|
+
if (issue.links.length || issue.subtasks.length) {
|
|
60
|
+
total -= linkCost[idx] ?? 0;
|
|
61
|
+
issue.links = [];
|
|
62
|
+
issue.subtasks = [];
|
|
63
|
+
overflow.add("links");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// 4. description - last thing to go before core metadata
|
|
68
|
+
if (!fits()) {
|
|
69
|
+
issues.forEach((issue, idx) => {
|
|
70
|
+
if (issue.description) {
|
|
71
|
+
total -= descriptionCost[idx] ?? 0;
|
|
72
|
+
delete issue.description;
|
|
73
|
+
overflow.add("description");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Core metadata is never dropped. If it alone exceeds the budget we still
|
|
78
|
+
// return it and report the result as incomplete rather than truncating rows.
|
|
79
|
+
if (!fits())
|
|
80
|
+
overflow.add("output_budget_exceeded");
|
|
81
|
+
return {
|
|
82
|
+
issues,
|
|
83
|
+
overflow: [...overflow],
|
|
84
|
+
complete: overflow.size === 0,
|
|
85
|
+
commentsComplete: !overflow.has("comments"),
|
|
86
|
+
linksComplete: !overflow.has("links"),
|
|
87
|
+
droppedComments,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function bare(issue) {
|
|
91
|
+
const { description: _d, comments: _c, history: _h, links: _l, subtasks: _s, ...rest } = issue;
|
|
92
|
+
return rest;
|
|
93
|
+
}
|
|
94
|
+
function sum(values) {
|
|
95
|
+
return values.reduce((a, b) => a + b, 0);
|
|
96
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProjectConfig } from "../config/schema.js";
|
|
2
|
+
export type SearchScope = "preview" | "complete";
|
|
3
|
+
export type PaginationPlan = {
|
|
4
|
+
pageSize: number;
|
|
5
|
+
/** Hard stop. Reaching it is reported via completeness metadata, never silently. */
|
|
6
|
+
maxPages: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Pagination is JAM's responsibility, not the agent's.
|
|
10
|
+
*
|
|
11
|
+
* `preview` returns the first page for interactive exploration.
|
|
12
|
+
* `complete` follows nextPageToken to the end, so a page-sized result is never
|
|
13
|
+
* mistaken for the full result set.
|
|
14
|
+
*/
|
|
15
|
+
export declare function paginationFor(scope: SearchScope, config: ProjectConfig): PaginationPlan;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination is JAM's responsibility, not the agent's.
|
|
3
|
+
*
|
|
4
|
+
* `preview` returns the first page for interactive exploration.
|
|
5
|
+
* `complete` follows nextPageToken to the end, so a page-sized result is never
|
|
6
|
+
* mistaken for the full result set.
|
|
7
|
+
*/
|
|
8
|
+
export function paginationFor(scope, config) {
|
|
9
|
+
return {
|
|
10
|
+
pageSize: config.search.pageSize,
|
|
11
|
+
maxPages: scope === "preview" ? 1 : config.search.maxPages,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials never leave this boundary in any log, telemetry line, or tool
|
|
3
|
+
* result. Only the adapter that builds the HTTP request may read `apiToken`.
|
|
4
|
+
*/
|
|
5
|
+
export type JiraCredentials = {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
email: string;
|
|
8
|
+
apiToken: string;
|
|
9
|
+
};
|
|
10
|
+
/** Where the resolved credentials came from - "mixed" when fields disagree. */
|
|
11
|
+
export type CredentialSource = "process" | "secret-store" | "user-env" | "mixed" | "none";
|
|
12
|
+
export type CredentialDescription = {
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
hasToken: boolean;
|
|
16
|
+
source: CredentialSource;
|
|
17
|
+
};
|
|
18
|
+
export interface CredentialPort {
|
|
19
|
+
/**
|
|
20
|
+
* Resolve and return credentials, or throw CONFIG_INVALID if a field is
|
|
21
|
+
* missing everywhere it was looked for. Format validation (e.g. the base URL
|
|
22
|
+
* scheme) is not this method's job - that lives in the boot health gate.
|
|
23
|
+
*/
|
|
24
|
+
load(): JiraCredentials;
|
|
25
|
+
/** Presence check for `jam doctor` / `jam serve` - must not return the secret itself. */
|
|
26
|
+
describe(): CredentialDescription;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { FullIssueContext, NormalizedComment } from "../domain/context.js";
|
|
2
|
+
/**
|
|
3
|
+
* One page of a JQL search. Pagination itself is NOT the adapter's decision -
|
|
4
|
+
* the adapter fetches exactly one page and reports the continuation token;
|
|
5
|
+
* PaginationPolicy in the application layer decides whether to keep going.
|
|
6
|
+
*/
|
|
7
|
+
export type SearchPageRequest = {
|
|
8
|
+
jql: string;
|
|
9
|
+
fields: string[];
|
|
10
|
+
pageSize: number;
|
|
11
|
+
pageToken?: string;
|
|
12
|
+
};
|
|
13
|
+
export type SearchPageResult = {
|
|
14
|
+
/** Partially populated: only the requested fields are set. */
|
|
15
|
+
issues: FullIssueContext[];
|
|
16
|
+
nextPageToken?: string;
|
|
17
|
+
responseBytes: number;
|
|
18
|
+
};
|
|
19
|
+
export type GetIssuesRequest = {
|
|
20
|
+
keys: string[];
|
|
21
|
+
fields: string[];
|
|
22
|
+
};
|
|
23
|
+
export type GetIssuesResult = {
|
|
24
|
+
issues: FullIssueContext[];
|
|
25
|
+
/** Keys that were requested but not returned (missing or not permitted). */
|
|
26
|
+
missingKeys: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Total comment count per issue key as Jira reports it. The embedded comment
|
|
29
|
+
* field only carries the first page, so FULL uses this to decide whether more
|
|
30
|
+
* pages must be fetched before the thread can be called complete.
|
|
31
|
+
*/
|
|
32
|
+
commentTotals: Record<string, number>;
|
|
33
|
+
responseBytes: number;
|
|
34
|
+
};
|
|
35
|
+
export type GetCommentsRequest = {
|
|
36
|
+
key: string;
|
|
37
|
+
startAt: number;
|
|
38
|
+
maxResults: number;
|
|
39
|
+
};
|
|
40
|
+
export type GetCommentsResult = {
|
|
41
|
+
comments: NormalizedComment[];
|
|
42
|
+
startAt: number;
|
|
43
|
+
total: number;
|
|
44
|
+
responseBytes: number;
|
|
45
|
+
};
|
|
46
|
+
export type CurrentUser = {
|
|
47
|
+
accountId: string;
|
|
48
|
+
displayName?: string;
|
|
49
|
+
emailAddress?: string;
|
|
50
|
+
};
|
|
51
|
+
export type ProjectRef = {
|
|
52
|
+
key: string;
|
|
53
|
+
name: string;
|
|
54
|
+
};
|
|
55
|
+
export type ListProjectsResult = {
|
|
56
|
+
projects: ProjectRef[];
|
|
57
|
+
/** True when more projects exist than were returned - advisory only, not paginated. */
|
|
58
|
+
truncated: boolean;
|
|
59
|
+
};
|
|
60
|
+
export interface JiraReadPort {
|
|
61
|
+
searchPage(req: SearchPageRequest): Promise<SearchPageResult>;
|
|
62
|
+
getIssues(req: GetIssuesRequest): Promise<GetIssuesResult>;
|
|
63
|
+
getComments(req: GetCommentsRequest): Promise<GetCommentsResult>;
|
|
64
|
+
/** Used by `jam doctor` to prove authentication works. */
|
|
65
|
+
getCurrentUser(): Promise<CurrentUser>;
|
|
66
|
+
/**
|
|
67
|
+
* Advisory only - used by `jam setup` to show the operator their options
|
|
68
|
+
* when no project key could be decided safely. Not part of the MCP tool
|
|
69
|
+
* contract and not held to the completeness/pagination guarantees that
|
|
70
|
+
* govern tool results.
|
|
71
|
+
*/
|
|
72
|
+
listProjects(): Promise<ListProjectsResult>;
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write path is out of scope for the first release - the structural boundary
|
|
3
|
+
* exists so adding writes later does not reshape the application layer.
|
|
4
|
+
* See ConsistencyPolicy: a write must be confirmed with a direct issue GET,
|
|
5
|
+
* never with a JQL search result.
|
|
6
|
+
*/
|
|
7
|
+
export interface JiraWritePort {
|
|
8
|
+
updateIssue(key: string, fields: Record<string, unknown>): Promise<void>;
|
|
9
|
+
addComment(key: string, body: string): Promise<{
|
|
10
|
+
id: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ToolMetrics = {
|
|
2
|
+
tool: string;
|
|
3
|
+
durationMs: number;
|
|
4
|
+
jiraRequests: number;
|
|
5
|
+
issues: number;
|
|
6
|
+
responseBytes: number;
|
|
7
|
+
pages?: number;
|
|
8
|
+
complete?: boolean;
|
|
9
|
+
errorCode?: string;
|
|
10
|
+
};
|
|
11
|
+
export interface TelemetryPort {
|
|
12
|
+
recordTool(metrics: ToolMetrics): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jam-mcp/server",
|
|
3
|
+
"version": "1.0.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": "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.0.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
|
+
}
|