jira-mcp-ai 0.9.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/CHANGELOG.md +139 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/bin/jira-mcp-ai.cjs +53 -0
- package/build/api/adf.d.ts +97 -0
- package/build/api/adf.js +1034 -0
- package/build/api/agile.d.ts +300 -0
- package/build/api/agile.js +695 -0
- package/build/api/attachments.d.ts +190 -0
- package/build/api/attachments.js +394 -0
- package/build/api/collab.d.ts +382 -0
- package/build/api/collab.js +852 -0
- package/build/api/filters.d.ts +113 -0
- package/build/api/filters.js +292 -0
- package/build/api/issues.d.ts +717 -0
- package/build/api/issues.js +1456 -0
- package/build/api/meta.d.ts +344 -0
- package/build/api/meta.js +547 -0
- package/build/api/search.d.ts +173 -0
- package/build/api/search.js +366 -0
- package/build/api/shared.d.ts +195 -0
- package/build/api/shared.js +324 -0
- package/build/api/users.d.ts +148 -0
- package/build/api/users.js +262 -0
- package/build/cli/doctor.d.ts +134 -0
- package/build/cli/doctor.js +1042 -0
- package/build/core/clock.d.ts +14 -0
- package/build/core/clock.js +75 -0
- package/build/core/config.d.ts +75 -0
- package/build/core/config.js +160 -0
- package/build/core/credentials.d.ts +84 -0
- package/build/core/credentials.js +115 -0
- package/build/core/env-lock.d.ts +78 -0
- package/build/core/env-lock.js +340 -0
- package/build/core/errors.d.ts +127 -0
- package/build/core/errors.js +340 -0
- package/build/core/host.d.ts +70 -0
- package/build/core/host.js +168 -0
- package/build/core/http-util.d.ts +160 -0
- package/build/core/http-util.js +533 -0
- package/build/core/http.d.ts +54 -0
- package/build/core/http.js +849 -0
- package/build/core/journal.d.ts +30 -0
- package/build/core/journal.js +143 -0
- package/build/core/log.d.ts +52 -0
- package/build/core/log.js +133 -0
- package/build/core/redact.d.ts +75 -0
- package/build/core/redact.js +307 -0
- package/build/core/rng.d.ts +8 -0
- package/build/core/rng.js +20 -0
- package/build/core/settings.d.ts +109 -0
- package/build/core/settings.js +496 -0
- package/build/core/telemetry.d.ts +33 -0
- package/build/core/telemetry.js +60 -0
- package/build/core/types.d.ts +566 -0
- package/build/core/types.js +167 -0
- package/build/index.d.ts +46 -0
- package/build/index.js +385 -0
- package/build/mcp/define.d.ts +76 -0
- package/build/mcp/define.js +212 -0
- package/build/mcp/errors.d.ts +42 -0
- package/build/mcp/errors.js +93 -0
- package/build/mcp/recent-writes.d.ts +68 -0
- package/build/mcp/recent-writes.js +136 -0
- package/build/mcp/registry.d.ts +76 -0
- package/build/mcp/registry.js +380 -0
- package/build/mcp/result.d.ts +80 -0
- package/build/mcp/result.js +411 -0
- package/build/mcp/server.d.ts +125 -0
- package/build/mcp/server.js +320 -0
- package/build/mcp/taint.d.ts +55 -0
- package/build/mcp/taint.js +126 -0
- package/build/mcp/tool-helpers.d.ts +131 -0
- package/build/mcp/tool-helpers.js +158 -0
- package/build/mcp/transport.d.ts +55 -0
- package/build/mcp/transport.js +146 -0
- package/build/mcp/types.d.ts +332 -0
- package/build/mcp/types.js +74 -0
- package/build/mcp/write-mode.d.ts +143 -0
- package/build/mcp/write-mode.js +426 -0
- package/build/tools/agile.d.ts +138 -0
- package/build/tools/agile.js +503 -0
- package/build/tools/attachments.d.ts +63 -0
- package/build/tools/attachments.js +497 -0
- package/build/tools/collab.d.ts +173 -0
- package/build/tools/collab.js +615 -0
- package/build/tools/core.d.ts +143 -0
- package/build/tools/core.js +129 -0
- package/build/tools/index.d.ts +62 -0
- package/build/tools/index.js +193 -0
- package/build/tools/issues-delete.d.ts +95 -0
- package/build/tools/issues-delete.js +319 -0
- package/build/tools/issues-write.d.ts +104 -0
- package/build/tools/issues-write.js +645 -0
- package/build/tools/issues.d.ts +41 -0
- package/build/tools/issues.js +312 -0
- package/build/tools/meta.d.ts +136 -0
- package/build/tools/meta.js +354 -0
- package/build/tools/search.d.ts +121 -0
- package/build/tools/search.js +413 -0
- package/build/tools/users.d.ts +51 -0
- package/build/tools/users.js +173 -0
- package/package.json +93 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
6
|
+
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
Because registrations pin an exact version (see README), this file is the thing a
|
|
9
|
+
user reads before bumping the pin. Entries describe what changes for **them** —
|
|
10
|
+
new or renamed tools, changed tool input/output shapes, changed defaults, changed
|
|
11
|
+
env var names — not internal refactors.
|
|
12
|
+
|
|
13
|
+
## [0.9.0] — 2026-08-17
|
|
14
|
+
|
|
15
|
+
The first release. Everything below is "added" relative to nothing, so this
|
|
16
|
+
entry is the whole surface rather than a diff — later entries will be diffs
|
|
17
|
+
against it.
|
|
18
|
+
|
|
19
|
+
It is `0.9.0` and not `1.0.0` deliberately. The surface is complete and the
|
|
20
|
+
gate is green, but this build has never been run against a real Atlassian
|
|
21
|
+
tenant — every test in it answers a fake. A pre-1.0 number is the honest way to
|
|
22
|
+
say so, and it keeps the semver promise unspent until a live run has been made:
|
|
23
|
+
under 1.0.0 the same discovery would have to arrive as a breaking major.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **52 tools in 10 packages**, registered as a set you choose with
|
|
28
|
+
`JIRA_TOOL_PACKAGES`:
|
|
29
|
+
- `core` (2) — `jira_capabilities`, `jira_get_myself`.
|
|
30
|
+
- `search` (4) — JQL search and approximate count, plus the saved-filter
|
|
31
|
+
reads `jira_list_filters` / `jira_get_filter`. Neither filter tool runs the
|
|
32
|
+
stored JQL; `jira_search` is the only tool that executes one.
|
|
33
|
+
- `issues` (5) — issue, comments, transitions, changelog and worklog reads.
|
|
34
|
+
- `issues-write` (8) — create, update, transition, add comment, edit comment,
|
|
35
|
+
assign, worklog, link.
|
|
36
|
+
- `issues-delete` (3) — delete issue, comment and worklog, behind the
|
|
37
|
+
irreversible tier below. Not in the `reader` profile and in no read-only
|
|
38
|
+
selection; `JIRA_PACKAGES_DENY=issues-delete` removes the surface entirely.
|
|
39
|
+
- `attachments` (3) — list attachment metadata, download a file to disk,
|
|
40
|
+
upload a file from disk.
|
|
41
|
+
- `collab` (12) — watchers, votes, components, versions and project roles:
|
|
42
|
+
the surface around an issue rather than inside it. Nothing in this package
|
|
43
|
+
deletes anything — "remove watcher" and "remove vote" are links their `add`
|
|
44
|
+
twin restores exactly.
|
|
45
|
+
- `meta` (6) — projects, fields, create-meta, statuses and link types.
|
|
46
|
+
- `users` (1) — `jira_search_users`.
|
|
47
|
+
- `agile` (8) — boards, sprints, sprint issues, move to sprint and to
|
|
48
|
+
backlog, plus the sprint lifecycle (create, start, close).
|
|
49
|
+
- **Markdown on the way in and on the way out.** `jira_get_issue` and
|
|
50
|
+
`jira_get_comments` take `format: "text" | "markdown"`, and every rich-text
|
|
51
|
+
input on the 7 write tools that has one (`description`, `body`, `comment`)
|
|
52
|
+
takes the same option. The default is `text` and its output is byte-identical
|
|
53
|
+
to having no option at all, so nothing you already call changes. The dialect
|
|
54
|
+
is deliberately narrow — headings, lists, fenced code, inline code, bold,
|
|
55
|
+
italic, links — and anything outside it degrades exactly the way plain-text
|
|
56
|
+
flattening always did. Two safety rules travel with it: a mention renders
|
|
57
|
+
one-way (`@name` out, literal text back in), and only `http(s):` and
|
|
58
|
+
`mailto:` links keep their href.
|
|
59
|
+
- **A plan/apply gate on all 25 write tools.** `JIRA_WRITE_MODE` defaults to
|
|
60
|
+
`plan`, where a write tool describes the change and returns a single-use
|
|
61
|
+
`plan_id` instead of performing it. Executing takes all three:
|
|
62
|
+
`JIRA_WRITE_MODE=apply`, an explicit `apply: true`, and that `plan_id` echoed
|
|
63
|
+
back with identical arguments. Ids die with the server process, and unsafe
|
|
64
|
+
writes are never replayed after an ambiguous failure.
|
|
65
|
+
- **An irreversible tier for the three deletes**, which the normal gate does
|
|
66
|
+
not cover. On top of plan → apply they need `JIRA_ALLOW_IRREVERSIBLE=true`:
|
|
67
|
+
a blanket `JIRA_WRITE_MODE=apply` is never enough, because the variable is
|
|
68
|
+
set by the person who starts the server and the model cannot fill it in.
|
|
69
|
+
Planning a delete always works, even on a server that will never permit the
|
|
70
|
+
apply, and a refusal costs you nothing — no request is made and the
|
|
71
|
+
`plan_id` is not consumed, so flipping the variable and restarting does not
|
|
72
|
+
mean re-planning. The plan carries a `before` snapshot of what the apply
|
|
73
|
+
would destroy (for an issue: its summary, status and subtask keys; for a
|
|
74
|
+
comment or worklog: author, timestamps and an excerpt of the text), and a
|
|
75
|
+
successful apply echoes that same snapshot back — Jira answers a delete with
|
|
76
|
+
an empty 204, so this is the only receipt there is.
|
|
77
|
+
- **Attachments, with the bytes on disk instead of in the conversation.**
|
|
78
|
+
`JIRA_MEDIA_DIR` is the one directory the server reads from and writes to.
|
|
79
|
+
Unset, the download and upload tools refuse with a configuration error and
|
|
80
|
+
make no Jira call at all; metadata listing needs no directory and keeps
|
|
81
|
+
working. A download sanitizes the Jira-supplied filename (it is tenant-authored
|
|
82
|
+
text, and `../../etc/passwd` lands as `passwd` inside your media directory),
|
|
83
|
+
never overwrites — a collision gets a suffixed name and the result tells you
|
|
84
|
+
it was renamed — and writes the file `0600`; the untouched original name is
|
|
85
|
+
still reported. An upload takes a plain file name inside that same directory
|
|
86
|
+
and **refuses** paths, `..` and subdirectories rather than rewriting them. Both
|
|
87
|
+
directions are size-capped, and an upload that fails ambiguously is never
|
|
88
|
+
retried for you.
|
|
89
|
+
- **Tenant text arrives branded.** Any result that can carry free text written
|
|
90
|
+
inside Jira — issue and comment bodies, changelogs, worklogs, filter
|
|
91
|
+
descriptions, attachment filenames, delete `before` snapshots — is marked
|
|
92
|
+
`_untrusted` with a note that its content is data, never instructions.
|
|
93
|
+
- **`jira-mcp-ai doctor`** — checks the configuration and probes the site, with
|
|
94
|
+
`--version` and `--help` alongside it. Its report goes to stdout only on these
|
|
95
|
+
non-server paths; under the server, stdout is the MCP protocol and every
|
|
96
|
+
diagnostic goes to stderr.
|
|
97
|
+
- **Credential profiles** — `JIRA_PROFILE_<NAME>_SITE` / `_EMAIL` /
|
|
98
|
+
`_API_TOKEN`, selected with `JIRA_ACTIVE_PROFILE`. `JIRA_LOCK_PROFILE`
|
|
99
|
+
defaults to `true`, so a single tool call cannot switch tenant unless you
|
|
100
|
+
unlock it deliberately.
|
|
101
|
+
- **Package trimming** — `JIRA_PACKAGES_DENY` wins over the selection (`core` is
|
|
102
|
+
force-re-added, so the server is never left with no tools), and
|
|
103
|
+
`JIRA_PACKAGES_READONLY` drops just the write-tier tools out of a package it
|
|
104
|
+
otherwise keeps.
|
|
105
|
+
- **Budgets and loop guards** — `JIRA_REQUEST_TIMEOUT_MS` (30000),
|
|
106
|
+
`JIRA_CALL_BUDGET_MS` (120000), `JIRA_HOST_CONCURRENCY` (4),
|
|
107
|
+
`JIRA_RETRY_ATTEMPTS` (3), `JIRA_MAX_RESULT_CHARS` (25000) and
|
|
108
|
+
`JIRA_MAX_PAGES` (20). Results are truncated to a stated budget rather than
|
|
109
|
+
returned whole.
|
|
110
|
+
- **Host allowlisting** — outbound requests go to the configured site and to
|
|
111
|
+
`JIRA_ALLOWED_HOSTS` entries only, matched exactly or by anchored regex. No
|
|
112
|
+
suffix matching. Redirects are not followed, with one deliberate exception:
|
|
113
|
+
an attachment download follows exactly one hop to the signed media URL Jira
|
|
114
|
+
answers with. That hop must be `https:` and not a private or link-local
|
|
115
|
+
address, and it carries no credentials — your API token is never sent to it.
|
|
116
|
+
- **Secret redaction and an optional write journal** — the API token is
|
|
117
|
+
registered with the redactor and never logged; `JIRA_JOURNAL_PATH` records
|
|
118
|
+
every write tool call as JSONL.
|
|
119
|
+
- **API token expiry warnings** — `JIRA_TOKEN_EXPIRES` makes startup and
|
|
120
|
+
`doctor` warn 30 days out.
|
|
121
|
+
- **Transport: stdio.** `JIRA_TRANSPORT`, `JIRA_HTTP_PORT` and `JIRA_HTTP_TOKEN`
|
|
122
|
+
are still read and validated, but the HTTP transport is deferred past 1.0:
|
|
123
|
+
starting with `JIRA_TRANSPORT=http` fails immediately with a message saying
|
|
124
|
+
so, rather than silently falling back to stdio.
|
|
125
|
+
- Distribution manifests: `server.json` for the MCP registry, `.claude-plugin/`
|
|
126
|
+
for Claude Code, and a release workflow that publishes with provenance — over
|
|
127
|
+
GitHub OIDC once a trusted publisher can be registered, which for a package's
|
|
128
|
+
own first version is not yet possible.
|
|
129
|
+
- Repository furniture: build, lint, format, coverage and CI configuration; the
|
|
130
|
+
`npm run check` gate; the docs consistency linter (`scripts/docs-lint.mjs`)
|
|
131
|
+
and the tarball-content assertion (`scripts/check-tarball.mjs`).
|
|
132
|
+
|
|
133
|
+
### Notes
|
|
134
|
+
|
|
135
|
+
- Jira Cloud REST v3 only, so issue and comment bodies are ADF. Users are
|
|
136
|
+
addressed by `accountId`; there is no name or email lookup shortcut.
|
|
137
|
+
- Search runs on `/rest/api/3/search/jql` with `nextPageToken` paging. The
|
|
138
|
+
legacy `/rest/api/3/search` endpoint was removed by Atlassian on 2025-08-01
|
|
139
|
+
and is not called.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Baev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# jira-mcp-ai
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server for **Jira Cloud**: it gives an
|
|
4
|
+
MCP-capable agent (Claude Code, Claude Desktop, or any other client) tools to
|
|
5
|
+
search, read and — behind an explicit gate — write Jira issues, using your own
|
|
6
|
+
Atlassian account and API token.
|
|
7
|
+
|
|
8
|
+
Two things shape the design:
|
|
9
|
+
|
|
10
|
+
- **Writes are gated.** Write tools default to describing what they *would* do
|
|
11
|
+
instead of doing it. Executing requires both an opt-in configuration and an
|
|
12
|
+
explicit flag on the call. See [`docs/THREAT-MODEL.md`](docs/THREAT-MODEL.md).
|
|
13
|
+
- **Jira content is untrusted input.** Issue text arrives from whoever filed the
|
|
14
|
+
ticket. It is labelled as data, never merged into the agent's instructions.
|
|
15
|
+
|
|
16
|
+
> **Status: implemented.** The specification in [`docs/`](docs/README.md) is
|
|
17
|
+
> normative and the code ships with it; drift is a bug. Nothing is published to
|
|
18
|
+
> npm yet, so the registration example below will not resolve until the first
|
|
19
|
+
> release.
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Node.js ≥ 22 (env files are read with `process.loadEnvFile()`, not dotenv)
|
|
24
|
+
- A Jira Cloud site and an Atlassian API token
|
|
25
|
+
|
|
26
|
+
## Registration
|
|
27
|
+
|
|
28
|
+
Put this in `.mcp.json` (project scope), or paste the inner `"jira"` object
|
|
29
|
+
into `claude mcp add-json jira '<object>'` — `claude mcp add` takes CLI
|
|
30
|
+
arguments, not JSON.
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"jira": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "jira-mcp-ai@0.9.0"],
|
|
38
|
+
"env": {
|
|
39
|
+
"JIRA_SITE": "mycompany",
|
|
40
|
+
"JIRA_EMAIL": "me@example.com",
|
|
41
|
+
"JIRA_API_TOKEN": "<api-token>",
|
|
42
|
+
"JIRA_WRITE_MODE": "plan"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The version is pinned on purpose: an unpinned `npx -y` re-resolves to whatever is
|
|
50
|
+
newest at spawn time, so a fresh publish could start running new code inside an
|
|
51
|
+
agent session with no review step. Bump the pin once you have read the changelog.
|
|
52
|
+
|
|
53
|
+
Before wiring the server into a client, run
|
|
54
|
+
`npx -y jira-mcp-ai@0.9.0 doctor` with the same environment variables set: it
|
|
55
|
+
runs the configuration and credential probes from a plain terminal and prints a
|
|
56
|
+
report — the fastest way to learn whether the site, email and token actually
|
|
57
|
+
work. `--help` and `--version` are also available.
|
|
58
|
+
|
|
59
|
+
**If the server does not appear, it is almost always PATH.** Claude Desktop
|
|
60
|
+
launches MCP servers from a minimal environment that does not include your
|
|
61
|
+
shell's PATH, so a `node`/`npx` installed by nvm, Homebrew or fnm is invisible to
|
|
62
|
+
it and the launch fails inside the client, before this server runs — you get the
|
|
63
|
+
client's generic "server failed" message and nothing on this server's stderr,
|
|
64
|
+
because there was no process. Fix it by giving an absolute path:
|
|
65
|
+
`"command": "/usr/local/bin/npx"` (`which npx` prints yours). Claude Code, run
|
|
66
|
+
from a terminal, inherits your PATH and is not affected.
|
|
67
|
+
|
|
68
|
+
Every diagnostic this server writes goes to **stderr**, never stdout — stdout is
|
|
69
|
+
the MCP protocol. Claude Code keeps it in `~/.claude/logs/`; Claude Desktop in
|
|
70
|
+
`~/Library/Logs/Claude/mcp*.log` (macOS) or `%APPDATA%\Claude\logs\` (Windows).
|
|
71
|
+
That is where the startup report and any `JIRA_*` configuration error will be.
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
Every setting is an environment variable with the `JIRA_` prefix. The full table
|
|
76
|
+
— names, defaults, required-ness, and where credentials may live — is in
|
|
77
|
+
[`docs/CONFIGURATION.md`](docs/CONFIGURATION.md); [`.env.example`](.env.example)
|
|
78
|
+
is a fill-in-the-blanks copy.
|
|
79
|
+
|
|
80
|
+
## Tools
|
|
81
|
+
|
|
82
|
+
<!-- GENERATED:TOOLS:START -->
|
|
83
|
+
|
|
84
|
+
<!-- Generated by scripts/generate-readme.mjs from the tool manifest.
|
|
85
|
+
Do not edit by hand — run `npm run readme`. -->
|
|
86
|
+
|
|
87
|
+
52 tools in 10 packages, 25 of them write tools
|
|
88
|
+
behind the plan/apply gate. `JIRA_TOOL_PACKAGES`, `JIRA_PACKAGES_DENY` and
|
|
89
|
+
`JIRA_PACKAGES_READONLY` decide which of them a session actually sees; the
|
|
90
|
+
`core` package is always registered. Full input and output shapes are in
|
|
91
|
+
[`docs/TOOLS.md`](docs/TOOLS.md).
|
|
92
|
+
|
|
93
|
+
### Core — `core`
|
|
94
|
+
|
|
95
|
+
Server self-description and credential check — always registered, even when every other package is denied.
|
|
96
|
+
|
|
97
|
+
| Tool | Title | Access | What it does |
|
|
98
|
+
| --- | --- | --- | --- |
|
|
99
|
+
| `jira_capabilities` | Describe this server | read-only | Describe this server without calling Jira: the packages and tools that are registered, the site, the active profile, the write mode (plan vs apply) and the per-call limits. |
|
|
100
|
+
| `jira_get_myself` | Get authenticated user | read-only | Verify the configured credentials and return the account they belong to: accountId, displayName, active, accountType, timeZone and locale. |
|
|
101
|
+
|
|
102
|
+
### Search — `search`
|
|
103
|
+
|
|
104
|
+
JQL search over issues, approximate result counts, and the saved filters that store reusable JQL — one page per call.
|
|
105
|
+
|
|
106
|
+
| Tool | Title | Access | What it does |
|
|
107
|
+
| --- | --- | --- | --- |
|
|
108
|
+
| `jira_search` | Search issues | read-only | Run JQL and return ONE page of issues plus data.nextPageToken — pass it back to read the next page. There is no total; use jira_count. |
|
|
109
|
+
| `jira_count` | Count issues | read-only | Count the issues a JQL matches without fetching any of them. |
|
|
110
|
+
| `jira_list_filters` | List saved filters | read-only | Finds saved filters — named, stored JQL — by name substring or owner, and returns each one with its jql. |
|
|
111
|
+
| `jira_get_filter` | Get saved filter | read-only | Reads one saved filter by numeric id (from jira_list_filters): name, description, owner, the JQL it stores and whether you favourited it. |
|
|
112
|
+
|
|
113
|
+
### Issues (read) — `issues`
|
|
114
|
+
|
|
115
|
+
One issue and its comments, available transitions, change history and worklogs — reads only; the matching writes live in issues-write.
|
|
116
|
+
|
|
117
|
+
| Tool | Title | Access | What it does |
|
|
118
|
+
| --- | --- | --- | --- |
|
|
119
|
+
| `jira_get_issue` | Get issue | read-only | Read one Jira issue by key or id. Name the fields you need — omitting fields returns Jira's whole navigable set and burns the result budget. |
|
|
120
|
+
| `jira_get_comments` | Get comments | read-only | List the comments on an issue, newest first by default (orderBy -created; Jira's own default is oldest first). |
|
|
121
|
+
| `jira_get_transitions` | Get transitions | read-only | List the workflow transitions available from this issue's CURRENT status: id, name and target status. |
|
|
122
|
+
| `jira_get_changelog` | Get changelog | read-only | Read an issue's change history — field, from → to, author, created. |
|
|
123
|
+
| `jira_get_worklogs` | Get worklogs | read-only | List the work logged on an issue: timeSpentSeconds, timeSpent, started, author and the flattened comment. Sum timeSpentSeconds rather than parsing timeSpent strings. |
|
|
124
|
+
|
|
125
|
+
### Issues (write) — `issues-write`
|
|
126
|
+
|
|
127
|
+
Issue changes: creation, fields, workflow transitions, comments and comment edits, assignee, worklogs and links — every tool is plan-gated.
|
|
128
|
+
|
|
129
|
+
| Tool | Title | Access | What it does |
|
|
130
|
+
| --- | --- | --- | --- |
|
|
131
|
+
| `jira_create_issue` | Create issue | write · standard | Create one issue. project and issueType are instance-specific — resolve them with jira_list_projects and jira_get_create_meta, which also names the custom fields this project requires. |
|
|
132
|
+
| `jira_update_issue` | Update issue | write · standard | Update fields on one issue. REPLACE semantics: description (text or ADF) replaces the WHOLE rich-text field, so tables and panels in the old value are lost — never "append" a paragraph this way. |
|
|
133
|
+
| `jira_transition_issue` | Transition issue | write · standard | Move one issue through its workflow. |
|
|
134
|
+
| `jira_add_comment` | Add comment | write · standard | Add a comment to one issue. body takes plain text (converted to ADF) or a raw ADF document; format: "markdown" parses a string body as the markdown subset. |
|
|
135
|
+
| `jira_update_comment` | Update comment | write · standard | Edit one existing comment. |
|
|
136
|
+
| `jira_assign_issue` | Assign issue | write · standard | Set or clear the assignee of one issue. Pass exactly one of accountId (assign) or unassign: true (clear) — both together is rejected as ambiguous, neither is rejected as intentless. |
|
|
137
|
+
| `jira_add_worklog` | Add worklog | write · standard | Log work against one issue. Pass exactly one of timeSpentSeconds (preferred) or timeSpent ("2h 30m"). |
|
|
138
|
+
| `jira_link_issues` | Link issues | write · standard | Link two issues. linkType is the link type NAME ("Blocks", "Relates"), and those names are instance-specific — read them from jira_list_link_types and spell them exactly. |
|
|
139
|
+
|
|
140
|
+
### Issues (delete) — `issues-delete`
|
|
141
|
+
|
|
142
|
+
Irreversible deletions: an issue, a comment, a worklog entry. Every tool is plan-gated AND needs JIRA_ALLOW_IRREVERSIBLE; every plan shows what would be destroyed.
|
|
143
|
+
|
|
144
|
+
| Tool | Title | Access | What it does |
|
|
145
|
+
| --- | --- | --- | --- |
|
|
146
|
+
| `jira_delete_issue` | Delete issue | write · irreversible | Permanently delete one issue. IRREVERSIBLE: Jira has no undo and no trash for this, the issue and its comments, worklogs and attachments are gone. |
|
|
147
|
+
| `jira_delete_comment` | Delete comment | write · irreversible | Permanently delete one comment from an issue. IRREVERSIBLE: the comment is not recoverable and the deletion is not recorded in the issue changelog. |
|
|
148
|
+
| `jira_delete_worklog` | Delete worklog | write · irreversible | Permanently delete one worklog entry from an issue. IRREVERSIBLE: the logged time is gone and Jira gives it back to the remaining estimate (its default adjustment). |
|
|
149
|
+
|
|
150
|
+
### Attachments — `attachments`
|
|
151
|
+
|
|
152
|
+
Files on issues: list what is attached, download one into the server's media directory, and attach a file from it. The two byte-moving tools need JIRA_MEDIA_DIR and never touch anything outside it.
|
|
153
|
+
|
|
154
|
+
| Tool | Title | Access | What it does |
|
|
155
|
+
| --- | --- | --- | --- |
|
|
156
|
+
| `jira_list_attachments` | List attachments | read-only | Lists the files attached to one issue: id, filename, size in bytes, mime type, author and creation time. Metadata only — no bytes are transferred and no local directory is needed. |
|
|
157
|
+
| `jira_download_attachment` | Download attachment | read-only | Downloads one attachment INTO THIS SERVER'S media directory and returns the local path — the bytes never pass through the conversation. |
|
|
158
|
+
| `jira_upload_attachment` | Upload attachment | write · standard | Attaches a file from this server's media directory to an issue. |
|
|
159
|
+
|
|
160
|
+
### Watchers, votes & project setup — `collab`
|
|
161
|
+
|
|
162
|
+
The surface around an issue: who watches it, who voted for it, and the components and versions a project files work under — including cutting a release. Reversible writes only; nothing here deletes anything.
|
|
163
|
+
|
|
164
|
+
| Tool | Title | Access | What it does |
|
|
165
|
+
| --- | --- | --- | --- |
|
|
166
|
+
| `jira_list_watchers` | List watchers | read-only | Lists the accounts watching an issue, with the watch count and whether this server's own account is among them. |
|
|
167
|
+
| `jira_add_watcher` | Add watcher | write · standard | Makes an account watch an issue, so Jira notifies it of every change. Adding an account that already watches changes nothing. |
|
|
168
|
+
| `jira_remove_watcher` | Remove watcher | write · standard | Stops an account watching an issue — it no longer gets notifications. Nothing is deleted: the watch is a link, jira_add_watcher puts it back with the same accountId, and no issue content changes. |
|
|
169
|
+
| `jira_add_vote` | Vote for issue | write · standard | Casts THIS SERVER'S OWN vote for an issue. There is no way to vote on behalf of another account — the endpoint takes no accountId — so a request to record someone else's vote cannot be honoured. |
|
|
170
|
+
| `jira_remove_vote` | Withdraw vote | write · standard | Withdraws THIS SERVER'S OWN vote from an issue; other people's votes are untouched and unreachable. Nothing is deleted beyond the vote itself, and jira_add_vote casts it again. |
|
|
171
|
+
| `jira_list_components` | List components | read-only | Lists a project's components — the sub-areas an issue's `components` field points at — with their id, name, description, lead and default assignee rule. |
|
|
172
|
+
| `jira_create_component` | Create component | write · standard | Creates a component in a project — a sub-area issues can be filed under. Takes the project KEY (jira_create_version takes a numeric id instead; that asymmetry is Jira's). |
|
|
173
|
+
| `jira_update_component` | Update component | write · standard | Changes a component. This is a PARTIAL update, unlike jira_update_issue: only the fields you pass are changed and everything you omit keeps its current value. |
|
|
174
|
+
| `jira_list_versions` | List versions | read-only | Lists a project's versions (releases) — the values an issue's fixVersions and affectedVersions fields point at — with their id, name, dates and whether they are released or archived. |
|
|
175
|
+
| `jira_create_version` | Create version | write · standard | Creates a version (a release) in a project — a value issues can then use in fixVersions. Takes the NUMERIC projectId, not the key (jira_create_component takes a key; the asymmetry is Jira's). |
|
|
176
|
+
| `jira_update_version` | Update version | write · standard | Changes a version — this is how a release is cut (released: true) and how it is un-cut (released: false). |
|
|
177
|
+
| `jira_list_project_roles` | List project roles | read-only | Lists a project's roles (Administrators, Developers, …) with their ids, and — when you pass a roleId — the accounts and groups in that one role. |
|
|
178
|
+
|
|
179
|
+
### Metadata & discovery — `meta`
|
|
180
|
+
|
|
181
|
+
Projects, fields, create metadata, statuses and link types — the reads that turn names into the ids every other package needs.
|
|
182
|
+
|
|
183
|
+
| Tool | Title | Access | What it does |
|
|
184
|
+
| --- | --- | --- | --- |
|
|
185
|
+
| `jira_list_projects` | List projects | read-only | Lists the Jira projects you can see — id, key, name, project type and lead. This is how a project NAME becomes the KEY every other tool wants. |
|
|
186
|
+
| `jira_get_project` | Get project | read-only | Reads one project in detail: description, lead, issue types, components and versions — what you need before creating an issue, because issue type ids and component/version names are per-project. |
|
|
187
|
+
| `jira_list_fields` | List fields | read-only | THE discovery tool for field ids: every field with id, name, schema type and the custom flag, so "Story Points" resolves to customfield_10016 and back. |
|
|
188
|
+
| `jira_get_create_meta` | Get create metadata | read-only | Reads what jira_create_issue accepts for a project. |
|
|
189
|
+
| `jira_list_statuses` | List statuses | read-only | Lists workflow statuses — id, name, category and scope — so JQL like status = "In Review" names a status that really exists on this site. |
|
|
190
|
+
| `jira_list_link_types` | List issue link types | read-only | Lists the issue link types configured on this site with their inward and outward phrases (for example "blocks" / "is blocked by"). |
|
|
191
|
+
|
|
192
|
+
### User lookup — `users`
|
|
193
|
+
|
|
194
|
+
Finding people by name or email — the one path from a human name to the accountId every other tool requires.
|
|
195
|
+
|
|
196
|
+
| Tool | Title | Access | What it does |
|
|
197
|
+
| --- | --- | --- | --- |
|
|
198
|
+
| `jira_search_users` | Search users | read-only | Finds Jira users by display name or email and returns their accountId — the id every other tool takes, since Cloud has no usernames. |
|
|
199
|
+
|
|
200
|
+
### Boards & sprints — `agile`
|
|
201
|
+
|
|
202
|
+
Jira Software boards, sprints and their issues, the two moves (into a sprint, out to the backlog) and the sprint lifecycle — the only tools that speak the Agile API rather than the platform one.
|
|
203
|
+
|
|
204
|
+
| Tool | Title | Access | What it does |
|
|
205
|
+
| --- | --- | --- | --- |
|
|
206
|
+
| `jira_list_boards` | List boards | read-only | Lists the Jira Software boards you can see — id, name, type and the project each belongs to. The board id is what jira_list_sprints takes, so this is the first call of any sprint workflow. |
|
|
207
|
+
| `jira_list_sprints` | List sprints | read-only | Lists a board's sprints with their id, name, state, goal and dates. Filter with state ("active" for the sprint in flight, "future" for the ones planned). |
|
|
208
|
+
| `jira_get_sprint_issues` | Get sprint issues | read-only | Lists the issues in one sprint, flattened exactly like jira_search: rich text as plain text, users as accountId + displayName. |
|
|
209
|
+
| `jira_move_to_sprint` | Move issues to sprint | write · standard | Moves up to 50 issues into a sprint — the only way to set a sprint, which is not an editable field on jira_update_issue. |
|
|
210
|
+
| `jira_move_to_backlog` | Move issues to backlog | write · standard | Sends up to 50 issues back to the backlog — Jira defines it as "remove the future and active sprints from these issues", so it is the inverse of jira_move_to_sprint and the only way to clear a sprint… |
|
|
211
|
+
| `jira_create_sprint` | Create sprint | write · standard | Creates a sprint on a Scrum board and returns its id. The sprint is created in the "future" state — this does NOT start it, jira_start_sprint does, and only a started sprint is the work in flight. |
|
|
212
|
+
| `jira_start_sprint` | Start sprint | write · standard | Starts a sprint: "future" becomes "active", which is what makes its issues the work in flight and what every board report measures from. |
|
|
213
|
+
| `jira_close_sprint` | Close sprint | write · standard | Completes the active sprint. |
|
|
214
|
+
|
|
215
|
+
<!-- GENERATED:TOOLS:END -->
|
|
216
|
+
|
|
217
|
+
## Data handling
|
|
218
|
+
|
|
219
|
+
What leaves your machine, what is written to disk, what is redacted from logs,
|
|
220
|
+
and what the write gate does and does not promise are documented in
|
|
221
|
+
[`docs/THREAT-MODEL.md`](docs/THREAT-MODEL.md). Credential storage and lifecycle
|
|
222
|
+
are in [`docs/AUTH.md`](docs/AUTH.md).
|
|
223
|
+
|
|
224
|
+
To report a vulnerability, see [`SECURITY.md`](SECURITY.md).
|
|
225
|
+
|
|
226
|
+
## Development
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
npm install
|
|
230
|
+
npm run check # typecheck, lint, format, build, tarball, test, docs-lint, prod audit
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md) is the contributor entry point: the
|
|
234
|
+
`npm run check` gate, the rules a PR cannot break, and how to point the server
|
|
235
|
+
at a real Jira site without endangering anyone's tenant.
|
|
236
|
+
[`docs/README.md`](docs/README.md) is the index to the specification and
|
|
237
|
+
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) is the place to start; the test
|
|
238
|
+
taxonomy and the coverage gate are in [`docs/TESTING.md`](docs/TESTING.md).
|
|
239
|
+
Participation is governed by the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
[MIT](LICENSE).
|
|
244
|
+
|
|
245
|
+
Jira and Atlassian are trademarks of Atlassian Pty Ltd. This project is an
|
|
246
|
+
independent, unofficial client and is not affiliated with or endorsed by
|
|
247
|
+
Atlassian.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CommonJS launcher whose only job is the Node version guard. It must stay
|
|
3
|
+
// parseable by ancient Node (no arrow functions, no `?.`/`??`, no ESM syntax):
|
|
4
|
+
// the real entry is an ESM graph that old Node fails to *parse*, so a guard
|
|
5
|
+
// living inside it could never run. `import()` is deliberately the one modern
|
|
6
|
+
// construct here — it has been parseable since Node 12, long before the
|
|
7
|
+
// runtimes this guard exists to warn about.
|
|
8
|
+
//
|
|
9
|
+
// Failure sets `process.exitCode` and returns; it never calls `process.exit()`.
|
|
10
|
+
// Every failure here is a diagnostic on stderr, and under an MCP client stderr
|
|
11
|
+
// is a pipe: `process.exit()` tears the process down with the write still in the
|
|
12
|
+
// pipe buffer, which silently truncates the message at 64 KiB (measured). The
|
|
13
|
+
// same rule governs `src/index.ts` (CC-91), and for the same reason — the one
|
|
14
|
+
// sentence explaining why the server did not start is the whole value of these
|
|
15
|
+
// paths, so it must survive.
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
var major = parseInt(process.versions.node.split('.')[0], 10);
|
|
19
|
+
if (major < 22) {
|
|
20
|
+
console.error(
|
|
21
|
+
'jira-mcp-ai requires Node.js >= 22, but this is ' +
|
|
22
|
+
process.versions.node +
|
|
23
|
+
'.\nNode 22 is the floor because the server loads env files with ' +
|
|
24
|
+
'process.loadEnvFile() instead of dotenv.\nUse a newer runtime, e.g.: nvm install 24 && nvm use 24',
|
|
25
|
+
);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
} else {
|
|
28
|
+
// Importing the entry module is not enough to start it: its self-run guard
|
|
29
|
+
// compares process.argv[1] against its own path, and here argv[1] is *this*
|
|
30
|
+
// shim (ARCHITECTURE.md §Dependency injection boundary). So call the exported
|
|
31
|
+
// entry point explicitly, or the binary exits 0 having done nothing at all.
|
|
32
|
+
import('../build/index.js')
|
|
33
|
+
.then(function (mod) {
|
|
34
|
+
var run = mod && mod.main;
|
|
35
|
+
if (typeof run !== 'function') {
|
|
36
|
+
console.error(
|
|
37
|
+
'jira-mcp-ai: build/index.js exports no main() entry point. ' +
|
|
38
|
+
'From a source checkout, run `npm run build` first.',
|
|
39
|
+
);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return run();
|
|
44
|
+
})
|
|
45
|
+
.catch(function (err) {
|
|
46
|
+
console.error(
|
|
47
|
+
'jira-mcp-ai: the compiled server could not be started. From a source ' +
|
|
48
|
+
'checkout, run `npm run build` first.\n' +
|
|
49
|
+
(err && err.stack ? err.stack : String(err)),
|
|
50
|
+
);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal ADF node. The real schema is far larger; we read a subset and emit
|
|
3
|
+
* a smaller one, so unknown keys are tolerated rather than modelled.
|
|
4
|
+
*/
|
|
5
|
+
export interface AdfNode {
|
|
6
|
+
type: string;
|
|
7
|
+
version?: number;
|
|
8
|
+
text?: string;
|
|
9
|
+
content?: AdfNode[];
|
|
10
|
+
attrs?: Record<string, unknown>;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
/** A whole ADF document. Jira accepts `version: 1` only (JIRA-API.md §ADF). */
|
|
14
|
+
export interface AdfDoc extends AdfNode {
|
|
15
|
+
type: 'doc';
|
|
16
|
+
version: 1;
|
|
17
|
+
content: AdfNode[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Hard recursion cap. ADF from a human tops out around a dozen levels; anything
|
|
21
|
+
* past this is either a generated document or an attack on the stack, and a
|
|
22
|
+
* `RangeError` from a blown stack would take the whole tool call with it.
|
|
23
|
+
* Reaching the cap emits {@link DEPTH_LIMIT_MARKER} and stops descending, so a
|
|
24
|
+
* deep tree degrades gracefully instead of failing. This also terminates a
|
|
25
|
+
* cyclic tree, which no shape check can rule out on wire data.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MAX_NODE_DEPTH = 64;
|
|
28
|
+
/**
|
|
29
|
+
* Indentation cap for nested lists. Past six levels the leading whitespace
|
|
30
|
+
* stops growing: further nesting still renders, it just shares the sixth
|
|
31
|
+
* level's indent. Unbounded indentation eats the result budget (CC-25) for a
|
|
32
|
+
* distinction no reader makes.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MAX_LIST_INDENT_DEPTH = 6;
|
|
35
|
+
/** What a subtree past {@link MAX_NODE_DEPTH} collapses to. */
|
|
36
|
+
export declare const DEPTH_LIMIT_MARKER = "[\u2026]";
|
|
37
|
+
/**
|
|
38
|
+
* Flatten an ADF document (or any subtree, or `null`) to readable plain text.
|
|
39
|
+
* Never throws: unknown nodes, malformed attrs, cycles and pathological depth
|
|
40
|
+
* all degrade (CC-06, CC-09). An empty or absent document is `''`, never
|
|
41
|
+
* `"undefined"` (CC-08).
|
|
42
|
+
*
|
|
43
|
+
* Delta over the donor: the result is right-trimmed, so a one-paragraph
|
|
44
|
+
* description is `'hello'` and not `'hello\n'`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function adfToText(node: unknown): string;
|
|
47
|
+
/**
|
|
48
|
+
* Flatten an ADF document to the markdown subset instead of plain text. Same
|
|
49
|
+
* guarantees as {@link adfToText} — never throws, same caps, same degradation
|
|
50
|
+
* for everything outside the subset — and the same right-trim, so a
|
|
51
|
+
* one-paragraph description is `'hello'`.
|
|
52
|
+
*
|
|
53
|
+
* This is opt-in per call (`format: 'markdown'`); plain text stays the default
|
|
54
|
+
* so existing clients keep byte-identical output.
|
|
55
|
+
*/
|
|
56
|
+
export declare function adfToMarkdown(node: unknown): string;
|
|
57
|
+
/**
|
|
58
|
+
* Replace every ADF document inside an arbitrary JSON value with its rendered
|
|
59
|
+
* string, non-mutatingly. This is how the tool ring re-renders a payload it
|
|
60
|
+
* fetched raw: the *set* of fields that gets rendered is exactly the set
|
|
61
|
+
* {@link isAdfDoc} recognises, which is the same set the plain-text shaping
|
|
62
|
+
* path flattens, so `format: 'markdown'` cannot render a field that
|
|
63
|
+
* `format: 'text'` would have left alone.
|
|
64
|
+
*/
|
|
65
|
+
export declare function renderAdfDocs<T>(value: T, render: (node: unknown) => string): T;
|
|
66
|
+
/**
|
|
67
|
+
* Build a minimal ADF document from plain text (CC-10):
|
|
68
|
+
* CRLF is normalised, a blank line starts a new paragraph, a single newline
|
|
69
|
+
* inside a paragraph becomes a `hardBreak`, and leading/trailing blank
|
|
70
|
+
* paragraphs are trimmed. The version is pinned — Jira accepts `1` only.
|
|
71
|
+
*
|
|
72
|
+
* Blank input yields a doc with an empty `content` array, which is how a
|
|
73
|
+
* rich-text field is cleared. Tools that require a non-empty body (a comment)
|
|
74
|
+
* validate their input; that is not this builder's job.
|
|
75
|
+
*/
|
|
76
|
+
export declare function adfFromText(text: string): AdfDoc;
|
|
77
|
+
/**
|
|
78
|
+
* Parse the markdown subset into an ADF document. The inverse of
|
|
79
|
+
* {@link adfToMarkdown} for everything the subset covers; anything else — block
|
|
80
|
+
* quotes, tables, images, reference links, setext headings, HTML — degrades to
|
|
81
|
+
* the paragraph text it was written as, never to an exception.
|
|
82
|
+
*
|
|
83
|
+
* CC-10 parity with {@link adfFromText} is deliberate: CRLF is normalised, a
|
|
84
|
+
* blank line starts a new paragraph, a single newline inside a paragraph is a
|
|
85
|
+
* `hardBreak`, and leading/trailing blank paragraphs are trimmed.
|
|
86
|
+
*/
|
|
87
|
+
export declare function adfFromMarkdown(text: string): AdfDoc;
|
|
88
|
+
/** Structural guard: is this value an ADF document rather than a scalar field? */
|
|
89
|
+
export declare function isAdfDoc(value: unknown): value is AdfDoc;
|
|
90
|
+
/**
|
|
91
|
+
* Accept either plain text or a caller-supplied ADF document for a rich-text
|
|
92
|
+
* field (TOOLS.md: `description` takes text or raw ADF, and either one replaces
|
|
93
|
+
* the whole field — CC-31). A string is built; a document is normalised to the
|
|
94
|
+
* pinned `{ type: 'doc', version: 1 }` header and otherwise passed through.
|
|
95
|
+
* Anything else is a `validation` error, not a silent coercion.
|
|
96
|
+
*/
|
|
97
|
+
export declare function toAdf(body: string | AdfNode): AdfDoc;
|