@simplysf/simply-atlassian-mcp 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @simplysf/simply-atlassian-mcp
2
+
3
+ [![NPM](https://img.shields.io/npm/v/@simplysf/simply-atlassian-mcp?label=@simplysf/simply-atlassian-mcp)](https://npmjs.com/@simplysf/simply-atlassian-mcp) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://raw.githubusercontent.com/SimplySF/simply-atlassian/main/LICENSE.txt)
4
+
5
+ A [Model Context Protocol](https://modelcontextprotocol.io/) server that gives an AI agent in
6
+ Claude Desktop, Claude Code, Cursor, or any other MCP client the same Jira and Confluence
7
+ capabilities as the [`@simplysf/simply-atlassian`](../simply-atlassian) CLI: one tool per command,
8
+ calling the same [`@simplysf/simply-atlassian-core`](../simply-atlassian-core) library in-process.
9
+ The agent gets the CLI's credential handling, `dryRun` previews, read-only guard, credential
10
+ redaction, and error messages, and the same raw JSON the CLI prints with `--json`, without a
11
+ process per call.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g @simplysf/simply-atlassian-mcp
17
+ ```
18
+
19
+ ## Configure an MCP client
20
+
21
+ The server speaks MCP over stdio. Point a client at the binary and give it the connection settings
22
+ the CLI reads — see the [Credentials](https://simplysf.github.io/simply-atlassian/guides/credentials/)
23
+ guide — either as environment variables or as a file. For Claude Desktop, in
24
+ `claude_desktop_config.json`:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "simply-atlassian": {
30
+ "command": "simply-atlassian-mcp",
31
+ "args": ["--env-file", "/home/me/atlassian.env"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ For Claude Code:
38
+
39
+ ```sh
40
+ claude mcp add simply-atlassian -- simply-atlassian-mcp --env-file ~/atlassian.env
41
+ ```
42
+
43
+ ## Read-only by default
44
+
45
+ Started without options, the server registers only the read tools. Start it with `--allow-writes`
46
+ to also register the tools that create, update, transition, comment, link, and delete. Even then,
47
+ `ATLASSIAN_READ_ONLY` in the environment refuses every write, exactly as it does for the CLI.
48
+
49
+ That mirrors the two-credential-file arrangement in
50
+ [Write safety](https://simplysf.github.io/simply-atlassian/guides/write-safety/): give the agent's
51
+ everyday server a read-scoped token, and configure a second server entry with `--allow-writes` and
52
+ a write-capable token only when a person means to let the agent write. A read-scoped token is
53
+ still the only layer that binds; the server-side default just keeps write tools out of the agent's
54
+ normal loop entirely.
55
+
56
+ With writes allowed:
57
+
58
+ - every write tool accepts `dryRun: true`, which returns the request that would be sent without
59
+ sending it;
60
+ - `jira_issue_delete` and `jira_issue_comment_delete` also require `confirm: true`, and so does
61
+ `confluence_page_delete` with `purge: true`. A call without it is refused before anything is
62
+ looked up. Trashing a page needs no `confirm`, because it is reversible.
63
+
64
+ ## Tools
65
+
66
+ | Read tools (always registered) | Write tools (`--allow-writes`) |
67
+ | ------------------------------ | ------------------------------------------------------- |
68
+ | `jira_whoami` | `jira_issue_create` |
69
+ | `jira_user_search` | `jira_issue_update` |
70
+ | `jira_user_view` | `jira_issue_transition` |
71
+ | `jira_open` | `jira_issue_delete` (needs `confirm`) |
72
+ | `jira_issue_search` | `jira_issue_comment_add` |
73
+ | `jira_issue_view` | `jira_issue_comment_edit` |
74
+ | `jira_issue_history` | `jira_issue_comment_delete` (needs `confirm`) |
75
+ | `jira_issue_transitions` | `jira_issue_link_create` |
76
+ | `jira_issue_comment_list` | `jira_issue_link_delete` |
77
+ | `jira_issue_link_list` | `jira_sprint_add` |
78
+ | `jira_issue_link_types` | `confluence_page_create` |
79
+ | `jira_board_list` | `confluence_page_update` |
80
+ | `jira_sprint_list` | `confluence_page_delete` (needs `confirm` with `purge`) |
81
+ | `jira_sprint_issues` | `confluence_page_comment_add` |
82
+ | `confluence_open` | |
83
+ | `confluence_page_get` | |
84
+ | `confluence_page_search` | |
85
+ | `confluence_page_children` | |
86
+ | `confluence_page_comment_list` | |
87
+
88
+ Each tool's inputs are the command's arguments and flags in camel case; see the CLI's
89
+ [command reference](https://simplysf.github.io/simply-atlassian/reference/) for what each does.
90
+ `fields` on the issue tools matters as much here as in the CLI: raw issue payloads are large, and
91
+ an agent pays for every token it reads. The two `open` tools return the URL rather than launching
92
+ a browser.
93
+
94
+ ## Results and errors
95
+
96
+ A successful call returns what the CLI prints with `--json`, verbatim. A failure returns an error
97
+ result whose text is a JSON object with a stable `code`:
98
+
99
+ | `code` | Meaning |
100
+ | ------------------ | --------------------------------------------------------------------------------- |
101
+ | `config` | Missing or contradictory settings, a refused write, bad input. CLI exit code 2. |
102
+ | `auth` | The instance rejected the credentials; carries the HTTP `status`. Exit code 3. |
103
+ | `error` | Any other failure, including an API error with its `status` and sanitised `body`. |
104
+ | `confirm-required` | A destructive tool was called without `confirm: true`. |
105
+
106
+ The first three carry the same `name`, `message`, and `exitCode` the CLI writes to stderr,
107
+ scrubbed the same way, as described in
108
+ [Scripts and agents](https://simplysf.github.io/simply-atlassian/guides/scripting/).
109
+
110
+ ## Options
111
+
112
+ ```
113
+ simply-atlassian-mcp [--allow-writes] [--env-file <path>]
114
+ ```
115
+
116
+ `--help` prints the options and the full tool list. Because stdout is the protocol stream, it
117
+ prints to stderr.
118
+
119
+ ## Issues
120
+
121
+ Please report any issues at https://github.com/SimplySF/simply-atlassian/issues
122
+
123
+ ## Contributing
124
+
125
+ This package is part of the [`@simplysf/simply-atlassian`](https://github.com/SimplySF/simply-atlassian) monorepo. See [CONTRIBUTING.md](CONTRIBUTING.md) for what's specific to this package, and the repo's [root CONTRIBUTING.md](https://github.com/SimplySF/simply-atlassian/blob/main/CONTRIBUTING.md) for repo structure, setup, commit conventions, and how to submit a pull request. Please also read our [Code of Conduct](https://github.com/SimplySF/simply-atlassian/blob/main/CODE_OF_CONDUCT.md).
126
+
127
+ ## License
128
+
129
+ Licensed under the [Apache-2.0](https://raw.githubusercontent.com/SimplySF/simply-atlassian/main/LICENSE.txt) license.
package/bin/run.js ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Stdio MCP servers own stdout as the protocol channel, so nothing here (or anywhere under
4
+ // src/) may write to it except the transport. Diagnostics go to stderr.
5
+ import { parseArgs } from 'node:util';
6
+
7
+ const { values } = parseArgs({
8
+ options: {
9
+ 'allow-writes': { type: 'boolean', default: false },
10
+ 'env-file': { type: 'string' },
11
+ help: { type: 'boolean', short: 'h', default: false },
12
+ },
13
+ strict: true,
14
+ });
15
+
16
+ const { selectTools, startServer } = await import('../lib/index.js');
17
+
18
+ if (values.help) {
19
+ const list = (tools) => tools.map((tool) => ` ${tool.name.padEnd(30)} ${tool.title}`).join('\n');
20
+ process.stderr.write(
21
+ [
22
+ 'Usage: simply-atlassian-mcp [--allow-writes] [--env-file <path>]',
23
+ '',
24
+ 'Serves Jira and Confluence as MCP tools over stdio, one tool per simply-atlassian CLI',
25
+ 'command, calling the same library in-process. Connection settings come from the',
26
+ 'environment (JIRA_URL, JIRA_USERNAME, JIRA_API_TOKEN, CONFLUENCE_*, ...) or --env-file.',
27
+ '',
28
+ ' --allow-writes Also register the tools that change or delete data (off by default).',
29
+ ' ATLASSIAN_READ_ONLY in the environment still refuses every write.',
30
+ ' --env-file <path> A .env file holding connection settings the environment does not.',
31
+ '',
32
+ 'Read tools (always registered):',
33
+ list(selectTools(false)),
34
+ '',
35
+ 'Write tools (with --allow-writes):',
36
+ list(selectTools(true).filter((tool) => tool.kind !== 'read')),
37
+ '',
38
+ ].join('\n'),
39
+ );
40
+ process.exit(0);
41
+ }
42
+
43
+ try {
44
+ await startServer({ allowWrites: values['allow-writes'], envFile: values['env-file'] });
45
+ } catch (error) {
46
+ // A missing or unreadable --env-file is the one failure that can happen before the transport
47
+ // is up; everything else is reported per tool call.
48
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
49
+ process.exit(typeof error?.exitCode === 'number' ? error.exitCode : 1);
50
+ }
@@ -0,0 +1,32 @@
1
+ import { type AtlassianConfig, ConfluenceClient, type EnvLike, JiraClient } from '@simplysf/simply-atlassian-core';
2
+ export interface ServerOptions {
3
+ /**
4
+ * Register the tools that change or delete data. Off by default, so a server launched
5
+ * without thinking about it can only read. Even when on, a write is still refused when the
6
+ * environment carries `ATLASSIAN_READ_ONLY`, exactly as the CLI refuses it.
7
+ */
8
+ readonly allowWrites?: boolean;
9
+ /**
10
+ * A `.env` file holding connection settings the host did not set. Loaded once, at startup,
11
+ * with the CLI's precedence: a variable already in the environment wins over the file.
12
+ */
13
+ readonly envFile?: string;
14
+ /** The environment to read settings from. Defaults to this process's; tests pass their own. */
15
+ readonly env?: EnvLike;
16
+ }
17
+ /**
18
+ * What every tool handler gets: the environment its settings come from, and clients built from
19
+ * it on demand. Clients are built per call rather than at startup so a missing or wrong setting
20
+ * is reported by the tool that needed it — the same moment the CLI would report it — and so a
21
+ * server configured for only one product still serves that product's tools.
22
+ */
23
+ export interface ToolContext {
24
+ readonly env: EnvLike;
25
+ readonly allowWrites: boolean;
26
+ jiraConfig(): AtlassianConfig;
27
+ jira(): JiraClient;
28
+ confluenceConfig(): AtlassianConfig;
29
+ confluence(): ConfluenceClient;
30
+ }
31
+ /** Builds the context, loading the env file first so every later lookup sees it. */
32
+ export declare function createContext(options?: ServerOptions): ToolContext;
package/lib/context.js ADDED
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import process from 'node:process';
17
+ import { ConfluenceClient, JiraClient, loadEnvFile, resolveConfluenceConfig, resolveJiraConfig, } from '@simplysf/simply-atlassian-core';
18
+ /** Builds the context, loading the env file first so every later lookup sees it. */
19
+ export function createContext(options = {}) {
20
+ const env = options.env ?? process.env;
21
+ if (options.envFile !== undefined)
22
+ loadEnvFile(options.envFile, env);
23
+ return {
24
+ env,
25
+ allowWrites: options.allowWrites ?? false,
26
+ jiraConfig: () => resolveJiraConfig({}, env),
27
+ jira: () => new JiraClient(resolveJiraConfig({}, env)),
28
+ confluenceConfig: () => resolveConfluenceConfig({}, env),
29
+ confluence: () => new ConfluenceClient(resolveConfluenceConfig({}, env)),
30
+ };
31
+ }
32
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAEL,gBAAgB,EAEhB,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,iBAAiB,GAClB,MAAM,iCAAiC,CAAC;AAiCzC,oFAAoF;AACpF,MAAM,UAAU,aAAa,CAAC,UAAyB,EAAE;IACvD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS;QAAE,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAErE,OAAO;QACL,GAAG;QACH,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,KAAK;QACzC,UAAU,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,GAAG,CAAC;QAC5C,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,UAAU,CAAC,iBAAiB,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACtD,gBAAgB,EAAE,GAAG,EAAE,CAAC,uBAAuB,CAAC,EAAE,EAAE,GAAG,CAAC;QACxD,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,gBAAgB,CAAC,uBAAuB,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;KACzE,CAAC;AACJ,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { createContext, type ServerOptions, type ToolContext } from './context.js';
2
+ export { createServer, invokeTool, mapError, selectTools, SERVER_NAME, SERVER_VERSION, startServer, type ToolError, type ToolErrorCode, } from './server.js';
3
+ export { TOOLS, type ToolKind, type ToolSpec } from './tools.js';
package/lib/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ // Everything exported from this file is this package's public API. The package is a stdio MCP
17
+ // server first (see bin/run.js); the programmatic surface exists so a host process can embed the
18
+ // same server over a transport of its choosing, and so tests can drive it in-memory.
19
+ export { createContext } from './context.js';
20
+ export { createServer, invokeTool, mapError, selectTools, SERVER_NAME, SERVER_VERSION, startServer, } from './server.js';
21
+ export { TOOLS } from './tools.js';
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,8FAA8F;AAC9F,iGAAiG;AACjG,qFAAqF;AACrF,OAAO,EAAE,aAAa,EAAwC,MAAM,cAAc,CAAC;AACnF,OAAO,EACL,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,WAAW,EACX,WAAW,EACX,cAAc,EACd,WAAW,GAGZ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,EAAgC,MAAM,YAAY,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3
+ import { type ServerOptions, type ToolContext } from './context.js';
4
+ import { type ToolSpec } from './tools.js';
5
+ /** The name this server announces during the MCP `initialize` handshake. */
6
+ export declare const SERVER_NAME = "simply-atlassian";
7
+ /** The version this server announces; always the package version, never a separate constant. */
8
+ export declare const SERVER_VERSION: string;
9
+ /**
10
+ * Stable, machine-readable failure categories. The first three mirror the CLI's exit codes
11
+ * (2, 3, and 1); `confirm-required` is this server's own gate.
12
+ */
13
+ export type ToolErrorCode = 'config' | 'auth' | 'error' | 'confirm-required';
14
+ /** The JSON object an `isError` result carries: the same fields as the CLI's `--json` error line. */
15
+ export interface ToolError {
16
+ readonly code: ToolErrorCode;
17
+ readonly name: string;
18
+ readonly message: string;
19
+ readonly exitCode: number;
20
+ /** HTTP status, when the instance answered with one. */
21
+ readonly status?: number;
22
+ /** The response body, sanitised the same way the message is, when the instance sent one. */
23
+ readonly body?: unknown;
24
+ }
25
+ /** Which tools a server with the given options registers. Exported so `--help` can list them. */
26
+ export declare function selectTools(allowWrites: boolean): readonly ToolSpec[];
27
+ /**
28
+ * Builds the MCP server with its metadata and tools registered, but not yet connected to a
29
+ * transport. Kept separate from {@link startServer} so tests can attach an in-memory transport
30
+ * and a host process can embed the server over whatever transport it runs.
31
+ */
32
+ export declare function createServer(options?: ServerOptions): McpServer;
33
+ /**
34
+ * Creates the server and serves it over stdio, which is how MCP clients such as Claude Desktop,
35
+ * Claude Code, and Cursor launch a local server. Resolves once the transport is connected; the
36
+ * process then stays alive until the client closes the stream.
37
+ */
38
+ export declare function startServer(options?: ServerOptions): Promise<McpServer>;
39
+ /**
40
+ * Runs one tool call end to end: the `confirm` gate, the read-only guard, the operation, and
41
+ * the mapping of a failure onto a tool result. Never throws for an operational failure — those
42
+ * are `isError` results the agent can read and act on — and only lets a genuine bug propagate.
43
+ */
44
+ export declare function invokeTool(spec: ToolSpec, context: ToolContext, input: Readonly<Record<string, unknown>>): Promise<CallToolResult>;
45
+ /**
46
+ * Turns a failure into the same object the CLI writes to stderr under `--json`, with the same
47
+ * scrubbing: the message and any response body are redacted of credentials and stripped of
48
+ * control characters before they reach an agent's context.
49
+ */
50
+ export declare function mapError(error: unknown, context: Pick<ToolContext, 'env'>): ToolError;
package/lib/server.js ADDED
@@ -0,0 +1,133 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { createRequire } from 'node:module';
17
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
+ import { assertWritesAllowed, AuthError, CliError, ConfigError, HttpError, redactSecrets, sanitiseDeep, secretValues, stripControl, } from '@simplysf/simply-atlassian-core';
20
+ import { createContext } from './context.js';
21
+ import { TOOLS } from './tools.js';
22
+ const { version } = createRequire(import.meta.url)('../package.json');
23
+ /** The name this server announces during the MCP `initialize` handshake. */
24
+ export const SERVER_NAME = 'simply-atlassian';
25
+ /** The version this server announces; always the package version, never a separate constant. */
26
+ export const SERVER_VERSION = version;
27
+ const INSTRUCTIONS = 'Tools mirror the simply-atlassian CLI one to one and return the raw Atlassian API payload as ' +
28
+ 'JSON text, calling the same library the CLI is built on. Only read tools are registered unless ' +
29
+ 'the server was started with --allow-writes. Every write tool accepts dryRun: true to preview ' +
30
+ 'the request without sending it, and the irreversible ones (issue and comment deletes, and a ' +
31
+ 'page purge) additionally require confirm: true. Failures come back as isError results whose ' +
32
+ 'text is JSON with a stable "code": config, auth, error, or confirm-required. Prefer "fields" ' +
33
+ 'on issue tools to keep payloads small.';
34
+ const ANNOTATIONS = {
35
+ read: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
36
+ write: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
37
+ destructive: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
38
+ };
39
+ /** Which tools a server with the given options registers. Exported so `--help` can list them. */
40
+ export function selectTools(allowWrites) {
41
+ return allowWrites ? TOOLS : TOOLS.filter((tool) => tool.kind === 'read');
42
+ }
43
+ /**
44
+ * Builds the MCP server with its metadata and tools registered, but not yet connected to a
45
+ * transport. Kept separate from {@link startServer} so tests can attach an in-memory transport
46
+ * and a host process can embed the server over whatever transport it runs.
47
+ */
48
+ export function createServer(options = {}) {
49
+ const context = createContext(options);
50
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { instructions: INSTRUCTIONS });
51
+ for (const spec of selectTools(context.allowWrites)) {
52
+ server.registerTool(spec.name, {
53
+ title: spec.title,
54
+ description: spec.description,
55
+ inputSchema: spec.inputSchema,
56
+ annotations: { title: spec.title, ...ANNOTATIONS[spec.kind] },
57
+ }, (input) => invokeTool(spec, context, input));
58
+ }
59
+ return server;
60
+ }
61
+ /**
62
+ * Creates the server and serves it over stdio, which is how MCP clients such as Claude Desktop,
63
+ * Claude Code, and Cursor launch a local server. Resolves once the transport is connected; the
64
+ * process then stays alive until the client closes the stream.
65
+ */
66
+ export async function startServer(options = {}) {
67
+ const server = createServer(options);
68
+ await server.connect(new StdioServerTransport());
69
+ return server;
70
+ }
71
+ /**
72
+ * Runs one tool call end to end: the `confirm` gate, the read-only guard, the operation, and
73
+ * the mapping of a failure onto a tool result. Never throws for an operational failure — those
74
+ * are `isError` results the agent can read and act on — and only lets a genuine bug propagate.
75
+ */
76
+ export async function invokeTool(spec, context, input) {
77
+ // Gated here, before anything is looked up, so a call without consent never reaches the
78
+ // instance even though the operation would refuse it too. `dryRun` is exempt: previewing
79
+ // needs no consent.
80
+ const needsConfirm = spec.kind === 'destructive' && (spec.requiresConfirm?.(input) ?? true);
81
+ if (needsConfirm && input.dryRun !== true && input.confirm !== true) {
82
+ return errorResult({
83
+ code: 'confirm-required',
84
+ name: 'ConfirmRequired',
85
+ message: `${spec.title} cannot be undone. Pass confirm: true to proceed, or dryRun: true to preview.`,
86
+ exitCode: 2,
87
+ });
88
+ }
89
+ try {
90
+ // The same guard the CLI applies before any write command runs, so a host that enables
91
+ // writes but points the server at a read-only credential file still gets the refusal.
92
+ if (spec.kind !== 'read')
93
+ assertWritesAllowed(context.env);
94
+ const result = await spec.run(context, input);
95
+ // Handed through untouched so the payload shape stays Atlassian's contract, not this server's.
96
+ return { content: [{ type: 'text', text: JSON.stringify(result ?? null) }] };
97
+ }
98
+ catch (error) {
99
+ return errorResult(mapError(error, context));
100
+ }
101
+ }
102
+ /**
103
+ * Turns a failure into the same object the CLI writes to stderr under `--json`, with the same
104
+ * scrubbing: the message and any response body are redacted of credentials and stripped of
105
+ * control characters before they reach an agent's context.
106
+ */
107
+ export function mapError(error, context) {
108
+ const secrets = secretValues(context.env);
109
+ const clean = (text) => stripControl(redactSecrets(text, secrets));
110
+ const message = clean(error instanceof Error ? error.message : String(error));
111
+ if (error instanceof ConfigError)
112
+ return { code: 'config', name: error.name, message, exitCode: error.exitCode };
113
+ if (error instanceof AuthError) {
114
+ return { code: 'auth', name: error.name, message, exitCode: error.exitCode, status: error.status };
115
+ }
116
+ if (error instanceof HttpError) {
117
+ return {
118
+ code: 'error',
119
+ name: error.name,
120
+ message,
121
+ exitCode: error.exitCode,
122
+ status: error.status,
123
+ body: sanitiseDeep(error.body, secrets),
124
+ };
125
+ }
126
+ if (error instanceof CliError)
127
+ return { code: 'error', name: error.name, message, exitCode: error.exitCode };
128
+ return { code: 'error', name: 'Error', message, exitCode: 1 };
129
+ }
130
+ function errorResult(error) {
131
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(error) }] };
132
+ }
133
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/lib/tools.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ import type { ToolContext } from './context.js';
3
+ /**
4
+ * What a tool does to the instance, which decides whether it is registered at all (only `read`
5
+ * tools without `--allow-writes`), which MCP annotations it carries, and whether `confirm` is
6
+ * demanded before it runs.
7
+ */
8
+ export type ToolKind = 'read' | 'write' | 'destructive';
9
+ export interface ToolSpec {
10
+ /** MCP tool name: `<product>_<noun>_<verb>`, snake case, stable across releases. */
11
+ readonly name: string;
12
+ readonly title: string;
13
+ readonly description: string;
14
+ /** The CLI command this tool is the equivalent of, e.g. `['atlassian', 'jira', 'issue', 'search']`. */
15
+ readonly command: readonly string[];
16
+ readonly kind: ToolKind;
17
+ readonly inputSchema: z.ZodRawShape;
18
+ /**
19
+ * For a destructive tool, whether this particular call needs `confirm`. Defaults to always;
20
+ * `confluence_page_delete` only needs it to purge, because trashing is reversible and the
21
+ * CLI does not demand `--confirm` for it either.
22
+ */
23
+ readonly requiresConfirm?: (input: Readonly<Record<string, unknown>>) => boolean;
24
+ /** Runs the tool against the instance and returns what the CLI would print under `--json`. */
25
+ readonly run: (context: ToolContext, input: Readonly<Record<string, unknown>>) => Promise<unknown>;
26
+ }
27
+ /**
28
+ * Every tool this server can register, one per CLI command. The `test/tools.test.ts` suite
29
+ * cross-checks this list against the CLI package's `command-snapshot.json`, so a command added
30
+ * to the CLI without a tool here fails CI rather than going quietly unexposed.
31
+ *
32
+ * Each handler returns exactly what the matching command returns under `--json`, and the logic
33
+ * between input and request is the core package's — the same functions the command calls — so
34
+ * a dry run, a refusal, or an error reads the same from either surface.
35
+ */
36
+ export declare const TOOLS: readonly ToolSpec[];