mcp-multi-jira 0.1.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 +7 -0
- package/README.md +143 -0
- package/dist/agents/install.js +279 -0
- package/dist/cli.js +576 -0
- package/dist/config/paths.js +14 -0
- package/dist/config/store.js +54 -0
- package/dist/keytar-f6bnxfss.node +0 -0
- package/dist/mcp/mock.js +63 -0
- package/dist/mcp/remote-session.js +189 -0
- package/dist/mcp/remoteSession.js +163 -0
- package/dist/mcp/server.js +266 -0
- package/dist/mcp/session-manager.js +62 -0
- package/dist/mcp/sessionManager.js +62 -0
- package/dist/mcp/types.js +1 -0
- package/dist/oauth/atlassian.js +272 -0
- package/dist/oauth/client-info-store.js +29 -0
- package/dist/oauth/clientInfoStore.js +29 -0
- package/dist/security/token-store.js +214 -0
- package/dist/security/tokenStore.js +204 -0
- package/dist/types.js +1 -0
- package/dist/utils/fs.js +28 -0
- package/dist/utils/log.js +30 -0
- package/dist/version.js +4 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 iipanda
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# mcp-multi-jira
|
|
2
|
+
|
|
3
|
+
[](https://github.com/iipanda/mcp-multi-jira/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Multi-account Jira MCP server. Configure multiple Atlassian Jira accounts and route tool calls by account alias.
|
|
6
|
+
|
|
7
|
+
## Project goals
|
|
8
|
+
|
|
9
|
+
Most Jira MCP setups are single-account, which gets painful if you regularly switch between:
|
|
10
|
+
|
|
11
|
+
- work + personal Jira
|
|
12
|
+
- multiple clients/tenants
|
|
13
|
+
- separate Jira sites with different permissions
|
|
14
|
+
|
|
15
|
+
This project exists to provide a **single MCP server** that can hold **multiple Jira accounts** and route every tool call via an explicit `account` parameter (so your agent config stays stable while you tell it to use different accounts in prompts).
|
|
16
|
+
|
|
17
|
+
This project has features like:
|
|
18
|
+
|
|
19
|
+
- optional encrypted or OS keychain token storage
|
|
20
|
+
- works with common MCP clients (Cursor, Claude Code, Codex CLI)
|
|
21
|
+
- automatic installation of MCP configs for supported agents
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
First, set up your account(s):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx -y mcp-multi-jira login Work
|
|
29
|
+
npx -y mcp-multi-jira login Personal
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then, install the MCP in your favorite coding agent:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx -y mcp-multi-jira install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That's it! Restart your coding agent / IDE to pick up the new MCP server.
|
|
39
|
+
|
|
40
|
+
## CLI reference
|
|
41
|
+
|
|
42
|
+
Log in an account:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
mcp-multi-jira login WorkJira
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
List accounts:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
mcp-multi-jira list
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Start the server:
|
|
55
|
+
(Note: server will be started automatically by the agents if you use the install command)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
mcp-multi-jira serve
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Install MCP configuration for supported agents:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
mcp-multi-jira install
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Advanced usage
|
|
68
|
+
|
|
69
|
+
### Change token storage
|
|
70
|
+
|
|
71
|
+
By default, tokens are stored in a plaintext file. To use encryption or the OS keychain, set the default backend once with:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Options: plain (default), encrypted, keychain
|
|
75
|
+
mcp-multi-jira token-store encrypted
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If you use encrypted storage, make sure your environment has the master password set, otherwise the MCP will fail to start:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
export MCP_JIRA_TOKEN_PASSWORD="your-master-password"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Plaintext tokens are stored at `~/.mcp-jira/tokens.json` (do not use on shared machines).
|
|
85
|
+
|
|
86
|
+
When switching token stores and accounts already exist, the CLI will prompt to migrate tokens to the new backend.
|
|
87
|
+
|
|
88
|
+
### Override OAuth client
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
export MCP_JIRA_CLIENT_ID="your-client-id"
|
|
92
|
+
export MCP_JIRA_CLIENT_SECRET="your-client-secret"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Manual agent configuration
|
|
96
|
+
|
|
97
|
+
You can auto-install agent configs:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
mcp-multi-jira install
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Below are manual snippets for advanced setups.
|
|
104
|
+
|
|
105
|
+
### Cursor
|
|
106
|
+
|
|
107
|
+
`~/.cursor/mcp.json` (or per-project `.cursor/mcp.json`):
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"mcpServers": {
|
|
112
|
+
"mcp-jira": {
|
|
113
|
+
"command": "npx",
|
|
114
|
+
"args": ["-y", "mcp-multi-jira", "serve"]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Claude Code
|
|
121
|
+
|
|
122
|
+
`~/.claude/mcp_servers.json`:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"mcpServers": {
|
|
127
|
+
"mcp-jira": {
|
|
128
|
+
"command": "npx",
|
|
129
|
+
"args": ["-y", "mcp-multi-jira", "serve"]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### OpenAI Codex CLI
|
|
136
|
+
|
|
137
|
+
`~/.codex/config.toml`:
|
|
138
|
+
|
|
139
|
+
```toml
|
|
140
|
+
[mcp_servers.mcp_jira]
|
|
141
|
+
command = "npx"
|
|
142
|
+
args = ["-y", "mcp-multi-jira", "serve"]
|
|
143
|
+
```
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import toml from "@iarna/toml";
|
|
5
|
+
import { checkbox, confirm, password } from "@inquirer/prompts";
|
|
6
|
+
import { backupFile } from "../utils/fs.js";
|
|
7
|
+
import { info, warn } from "../utils/log.js";
|
|
8
|
+
const MCP_SERVER_NAME = "mcp-jira";
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return Boolean(value) && typeof value === "object";
|
|
11
|
+
}
|
|
12
|
+
function ensureRecord(value) {
|
|
13
|
+
return isRecord(value) ? value : {};
|
|
14
|
+
}
|
|
15
|
+
function getOrCreateRecord(container, key) {
|
|
16
|
+
const current = container[key];
|
|
17
|
+
if (isRecord(current)) {
|
|
18
|
+
return current;
|
|
19
|
+
}
|
|
20
|
+
const next = {};
|
|
21
|
+
container[key] = next;
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
24
|
+
function createMcpServerEntry(env) {
|
|
25
|
+
return {
|
|
26
|
+
command: "npx",
|
|
27
|
+
args: ["-y", "mcp-multi-jira", "serve"],
|
|
28
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function resolveEnvDefaults() {
|
|
32
|
+
return {
|
|
33
|
+
clientId: process.env.MCP_JIRA_CLIENT_ID || process.env.ATLASSIAN_CLIENT_ID || "",
|
|
34
|
+
clientSecret: process.env.MCP_JIRA_CLIENT_SECRET ||
|
|
35
|
+
process.env.ATLASSIAN_CLIENT_SECRET ||
|
|
36
|
+
"",
|
|
37
|
+
tokenPassword: process.env.MCP_JIRA_TOKEN_PASSWORD || "",
|
|
38
|
+
tokenStore: process.env.MCP_JIRA_TOKEN_STORE || "",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function normalizeTokenStore(value) {
|
|
42
|
+
if (!value) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const normalized = value.toLowerCase();
|
|
46
|
+
if (normalized === "encrypted" ||
|
|
47
|
+
normalized === "plain" ||
|
|
48
|
+
normalized === "keychain") {
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
async function promptEnv(tokenStore) {
|
|
54
|
+
const defaults = resolveEnvDefaults();
|
|
55
|
+
if (tokenStore !== "encrypted") {
|
|
56
|
+
return { tokenPassword: "" };
|
|
57
|
+
}
|
|
58
|
+
if (defaults.tokenPassword) {
|
|
59
|
+
return { tokenPassword: defaults.tokenPassword };
|
|
60
|
+
}
|
|
61
|
+
const tokenPassword = await password({
|
|
62
|
+
message: "Master token password for encrypted storage (leave blank to skip):",
|
|
63
|
+
});
|
|
64
|
+
return { tokenPassword };
|
|
65
|
+
}
|
|
66
|
+
function buildEnv(envInput) {
|
|
67
|
+
const env = {};
|
|
68
|
+
if (envInput.clientId) {
|
|
69
|
+
env.MCP_JIRA_CLIENT_ID = envInput.clientId;
|
|
70
|
+
}
|
|
71
|
+
if (envInput.clientSecret) {
|
|
72
|
+
env.MCP_JIRA_CLIENT_SECRET = envInput.clientSecret;
|
|
73
|
+
}
|
|
74
|
+
if (envInput.tokenPassword) {
|
|
75
|
+
env.MCP_JIRA_TOKEN_PASSWORD = envInput.tokenPassword;
|
|
76
|
+
}
|
|
77
|
+
if (envInput.tokenStore) {
|
|
78
|
+
env.MCP_JIRA_TOKEN_STORE = String(envInput.tokenStore);
|
|
79
|
+
}
|
|
80
|
+
if (envInput.tokenStore === "keychain") {
|
|
81
|
+
env.MCP_JIRA_USE_KEYCHAIN = "true";
|
|
82
|
+
}
|
|
83
|
+
return env;
|
|
84
|
+
}
|
|
85
|
+
function isJiraEntry(name, entry) {
|
|
86
|
+
const lowered = name.toLowerCase();
|
|
87
|
+
if (lowered.includes("jira") || lowered.includes("atlassian")) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const serialized = JSON.stringify(entry ?? "").toLowerCase();
|
|
91
|
+
return (serialized.includes("mcp.atlassian.com") ||
|
|
92
|
+
serialized.includes("mcp-atlassian") ||
|
|
93
|
+
serialized.includes("jira_url"));
|
|
94
|
+
}
|
|
95
|
+
async function removeJiraEntries(entries, message, configPath) {
|
|
96
|
+
const jiraKeys = Object.keys(entries).filter((key) => isJiraEntry(key, entries[key]));
|
|
97
|
+
if (jiraKeys.length === 0) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const remove = await confirm({
|
|
101
|
+
message: `${message} (${jiraKeys.join(", ")}). Remove them?`,
|
|
102
|
+
default: false,
|
|
103
|
+
});
|
|
104
|
+
if (!remove) {
|
|
105
|
+
warn(`Skipping config update for ${configPath}.`);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
await backupFile(configPath);
|
|
109
|
+
for (const key of jiraKeys) {
|
|
110
|
+
delete entries[key];
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
async function readJsonConfig(filePath) {
|
|
115
|
+
try {
|
|
116
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
117
|
+
return { config: ensureRecord(JSON.parse(raw)), exists: true };
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (err.code === "ENOENT") {
|
|
121
|
+
return { config: {}, exists: false };
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function loadClaudeConfig() {
|
|
127
|
+
const home = os.homedir();
|
|
128
|
+
const mcpServersPath = path.join(home, ".claude", "mcp_servers.json");
|
|
129
|
+
const globalConfigPath = path.join(home, ".claude.json");
|
|
130
|
+
const mcpFile = await readJsonConfig(mcpServersPath);
|
|
131
|
+
if (mcpFile.exists) {
|
|
132
|
+
return { config: mcpFile.config, targetPath: mcpServersPath, mode: "mcp" };
|
|
133
|
+
}
|
|
134
|
+
const globalFile = await readJsonConfig(globalConfigPath);
|
|
135
|
+
return {
|
|
136
|
+
config: globalFile.config,
|
|
137
|
+
targetPath: globalConfigPath,
|
|
138
|
+
mode: "project",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function updateCursorConfig(configPath, env) {
|
|
142
|
+
let config = { mcpServers: {} };
|
|
143
|
+
let exists = false;
|
|
144
|
+
try {
|
|
145
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
146
|
+
config = ensureRecord(JSON.parse(raw));
|
|
147
|
+
exists = true;
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
if (err.code !== "ENOENT") {
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const mcpServers = getOrCreateRecord(config, "mcpServers");
|
|
155
|
+
if (exists) {
|
|
156
|
+
const shouldContinue = await removeJiraEntries(mcpServers, `Cursor config ${configPath} contains Jira MCP entries`, configPath);
|
|
157
|
+
if (!shouldContinue) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
mcpServers[MCP_SERVER_NAME] = createMcpServerEntry(env);
|
|
162
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
163
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
164
|
+
info(`Updated Cursor MCP config: ${configPath}`);
|
|
165
|
+
}
|
|
166
|
+
async function updateCodexConfig(configPath, env) {
|
|
167
|
+
let config = {};
|
|
168
|
+
let exists = false;
|
|
169
|
+
try {
|
|
170
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
171
|
+
config = ensureRecord(toml.parse(raw));
|
|
172
|
+
exists = true;
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
if (err.code !== "ENOENT") {
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const servers = getOrCreateRecord(config, "mcp_servers");
|
|
180
|
+
if (exists) {
|
|
181
|
+
const shouldContinue = await removeJiraEntries(servers, "Codex config has Jira MCP entries", configPath);
|
|
182
|
+
if (!shouldContinue) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
servers[MCP_SERVER_NAME.replace(/-/g, "_")] = createMcpServerEntry(env);
|
|
187
|
+
config.mcp_servers = servers;
|
|
188
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
189
|
+
await fs.writeFile(configPath, toml.stringify(config), "utf8");
|
|
190
|
+
info(`Updated Codex MCP config: ${configPath}`);
|
|
191
|
+
}
|
|
192
|
+
async function updateClaudeMcpServers(config, targetPath, env) {
|
|
193
|
+
const mcpServers = getOrCreateRecord(config, "mcpServers");
|
|
194
|
+
const shouldContinue = await removeJiraEntries(mcpServers, "Claude MCP config has Jira entries", targetPath);
|
|
195
|
+
if (!shouldContinue) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
mcpServers[MCP_SERVER_NAME] = createMcpServerEntry(env);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
async function updateClaudeProjectConfig(config, targetPath, env) {
|
|
202
|
+
const projectPath = process.cwd();
|
|
203
|
+
const projects = getOrCreateRecord(config, "projects");
|
|
204
|
+
const project = getOrCreateRecord(projects, projectPath);
|
|
205
|
+
const mcpServers = getOrCreateRecord(project, "mcpServers");
|
|
206
|
+
const shouldContinue = await removeJiraEntries(mcpServers, `Claude config for ${projectPath} has Jira entries`, targetPath);
|
|
207
|
+
if (!shouldContinue) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
mcpServers[MCP_SERVER_NAME] = createMcpServerEntry(env);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
async function updateClaudeConfig(env) {
|
|
214
|
+
const { config, targetPath, mode } = await loadClaudeConfig();
|
|
215
|
+
const updated = mode === "mcp"
|
|
216
|
+
? await updateClaudeMcpServers(config, targetPath, env)
|
|
217
|
+
: await updateClaudeProjectConfig(config, targetPath, env);
|
|
218
|
+
if (!updated) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
222
|
+
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), "utf8");
|
|
223
|
+
info(`Updated Claude MCP config: ${targetPath}`);
|
|
224
|
+
}
|
|
225
|
+
export async function runInstaller(options) {
|
|
226
|
+
const targets = (await checkbox({
|
|
227
|
+
message: "Select which agents to configure:",
|
|
228
|
+
choices: [
|
|
229
|
+
{ name: "Cursor", value: "cursor" },
|
|
230
|
+
{ name: "OpenAI Codex CLI", value: "codex" },
|
|
231
|
+
{ name: "Claude Code CLI", value: "claude" },
|
|
232
|
+
],
|
|
233
|
+
required: true,
|
|
234
|
+
}));
|
|
235
|
+
const defaults = resolveEnvDefaults();
|
|
236
|
+
const envStore = normalizeTokenStore(options?.tokenStore) ??
|
|
237
|
+
normalizeTokenStore(process.env.MCP_JIRA_TOKEN_STORE) ??
|
|
238
|
+
(process.env.MCP_JIRA_USE_KEYCHAIN ? "keychain" : null) ??
|
|
239
|
+
normalizeTokenStore(defaults.tokenStore) ??
|
|
240
|
+
"plain";
|
|
241
|
+
const envInput = await promptEnv(envStore);
|
|
242
|
+
const env = buildEnv({
|
|
243
|
+
...envInput,
|
|
244
|
+
tokenStore: envStore,
|
|
245
|
+
clientId: defaults.clientId,
|
|
246
|
+
clientSecret: defaults.clientSecret,
|
|
247
|
+
});
|
|
248
|
+
for (const target of targets) {
|
|
249
|
+
if (target === "cursor") {
|
|
250
|
+
const home = os.homedir();
|
|
251
|
+
const globalPath = path.join(home, ".cursor", "mcp.json");
|
|
252
|
+
const localPath = path.join(process.cwd(), ".cursor", "mcp.json");
|
|
253
|
+
await updateCursorConfig(globalPath, env);
|
|
254
|
+
try {
|
|
255
|
+
await fs.access(localPath);
|
|
256
|
+
const updateLocal = await confirm({
|
|
257
|
+
message: `Also update local Cursor config at ${localPath}?`,
|
|
258
|
+
default: false,
|
|
259
|
+
});
|
|
260
|
+
if (updateLocal) {
|
|
261
|
+
await updateCursorConfig(localPath, env);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// ignore missing local config
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (target === "codex") {
|
|
270
|
+
const configPath = path.join(os.homedir(), ".codex", "config.toml");
|
|
271
|
+
await updateCodexConfig(configPath, env);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (target === "claude") {
|
|
275
|
+
await updateClaudeConfig(env);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
info("Installation complete. Restart each agent to load the new MCP configuration.");
|
|
279
|
+
}
|