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/dist/cli.js ADDED
@@ -0,0 +1,576 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from "node:fs";
3
+ import { confirm, select } from "@inquirer/prompts";
4
+ import { Command } from "commander";
5
+ import { runInstaller } from "./agents/install.js";
6
+ import { plainTokenFilePath, tokenFilePath } from "./config/paths.js";
7
+ import { loadConfig, removeAccount, setAccount, setTokenStore, } from "./config/store.js";
8
+ import { RemoteSession } from "./mcp/remote-session.js";
9
+ import { startLocalServer } from "./mcp/server.js";
10
+ import { SessionManager } from "./mcp/session-manager.js";
11
+ import { DEFAULT_SCOPES, getStaticClientInfoFromEnv, loginWithDynamicOAuth, } from "./oauth/atlassian.js";
12
+ import { createTokenStore, getAuthStatusForAlias, } from "./security/token-store.js";
13
+ import { info, setLogTarget, warn } from "./utils/log.js";
14
+ import { PACKAGE_VERSION } from "./version.js";
15
+ const SCOPE_SPLIT_RE = /[ ,]+/;
16
+ const RESOURCE_ARRAY_KEYS = [
17
+ "resources",
18
+ "values",
19
+ "items",
20
+ "sites",
21
+ "data",
22
+ ];
23
+ function parseScopes(scopes) {
24
+ if (!scopes) {
25
+ return DEFAULT_SCOPES;
26
+ }
27
+ return scopes
28
+ .split(SCOPE_SPLIT_RE)
29
+ .map((scope) => scope.trim())
30
+ .filter(Boolean);
31
+ }
32
+ function resolveScopes(scopes) {
33
+ return parseScopes(scopes || process.env.MCP_JIRA_SCOPES);
34
+ }
35
+ function normalizeTokenStore(value) {
36
+ if (!value) {
37
+ return null;
38
+ }
39
+ const normalized = value.toLowerCase();
40
+ if (normalized === "encrypted" ||
41
+ normalized === "plain" ||
42
+ normalized === "keychain") {
43
+ return normalized;
44
+ }
45
+ return null;
46
+ }
47
+ function resolveTokenStoreFromConfig(config) {
48
+ const envStore = normalizeTokenStore(process.env.MCP_JIRA_TOKEN_STORE);
49
+ if (envStore) {
50
+ return envStore;
51
+ }
52
+ const envUseKeychain = process.env.MCP_JIRA_USE_KEYCHAIN === "1" ||
53
+ process.env.MCP_JIRA_USE_KEYCHAIN === "true";
54
+ if (envUseKeychain) {
55
+ return "keychain";
56
+ }
57
+ if (config.tokenStore) {
58
+ return config.tokenStore;
59
+ }
60
+ const plainExists = existsSync(plainTokenFilePath());
61
+ const encryptedExists = existsSync(tokenFilePath());
62
+ if (plainExists && !encryptedExists) {
63
+ return "plain";
64
+ }
65
+ if (encryptedExists && !plainExists) {
66
+ return "encrypted";
67
+ }
68
+ if (plainExists && encryptedExists) {
69
+ return "plain";
70
+ }
71
+ return "plain";
72
+ }
73
+ function describeTokenStore(store) {
74
+ if (store === "encrypted") {
75
+ return "encrypted file";
76
+ }
77
+ if (store === "plain") {
78
+ return "plaintext file";
79
+ }
80
+ return "keychain";
81
+ }
82
+ async function migrateTokenStore(options) {
83
+ const fromStore = await createTokenStore({ store: options.from });
84
+ const toStore = await createTokenStore({ store: options.to });
85
+ let migrated = 0;
86
+ let alreadyPresent = 0;
87
+ let missing = 0;
88
+ for (const alias of options.aliases) {
89
+ const tokens = await fromStore.get(alias);
90
+ if (!tokens) {
91
+ const existing = await toStore.get(alias);
92
+ if (existing) {
93
+ alreadyPresent += 1;
94
+ }
95
+ else {
96
+ missing += 1;
97
+ }
98
+ continue;
99
+ }
100
+ await toStore.set(alias, tokens);
101
+ await fromStore.remove(alias);
102
+ migrated += 1;
103
+ }
104
+ return { migrated, alreadyPresent, missing };
105
+ }
106
+ function formatAccounts(accounts, statusMap) {
107
+ if (accounts.length === 0) {
108
+ return "No accounts configured.";
109
+ }
110
+ const headers = ["Alias", "Site", "User", "Auth"];
111
+ const rows = accounts.map((account) => [
112
+ account.alias,
113
+ account.site,
114
+ account.user ?? "",
115
+ statusMap.get(account.alias) ?? "unknown",
116
+ ]);
117
+ const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
118
+ const formatRow = (row) => row
119
+ .map((cell, index) => cell.padEnd(widths[index]))
120
+ .join(" ")
121
+ .trimEnd();
122
+ return [
123
+ formatRow(headers),
124
+ formatRow(widths.map((w) => "-".repeat(w))),
125
+ ...rows.map(formatRow),
126
+ ].join("\n");
127
+ }
128
+ function formatAuthStatus(status) {
129
+ switch (status.status) {
130
+ case "ok":
131
+ return "ok";
132
+ case "missing":
133
+ return "needs login";
134
+ case "expired":
135
+ return "expired";
136
+ case "locked":
137
+ return "locked";
138
+ default:
139
+ return "unknown";
140
+ }
141
+ }
142
+ function isRecord(value) {
143
+ return Boolean(value) && typeof value === "object";
144
+ }
145
+ function extractStructuredContent(result) {
146
+ if ("structuredContent" in result && result.structuredContent) {
147
+ return result.structuredContent;
148
+ }
149
+ return null;
150
+ }
151
+ function extractToolResult(result) {
152
+ if ("toolResult" in result) {
153
+ return result.toolResult;
154
+ }
155
+ return null;
156
+ }
157
+ function extractTextItems(result) {
158
+ const raw = result.content;
159
+ if (!Array.isArray(raw)) {
160
+ return [];
161
+ }
162
+ const items = [];
163
+ for (const entry of raw) {
164
+ if (!isRecord(entry)) {
165
+ continue;
166
+ }
167
+ const type = typeof entry.type === "string" ? entry.type : "";
168
+ const text = typeof entry.text === "string" ? entry.text : "";
169
+ if (type === "text" && text) {
170
+ items.push({ type, text });
171
+ }
172
+ }
173
+ return items;
174
+ }
175
+ function parseJsonPayload(text) {
176
+ const trimmed = text.trim();
177
+ if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) {
178
+ return null;
179
+ }
180
+ try {
181
+ return JSON.parse(trimmed);
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ }
187
+ function extractStructuredResult(result) {
188
+ if (!isRecord(result)) {
189
+ return null;
190
+ }
191
+ const structured = extractStructuredContent(result);
192
+ if (structured) {
193
+ return structured;
194
+ }
195
+ const toolResult = extractToolResult(result);
196
+ if (toolResult) {
197
+ return toolResult;
198
+ }
199
+ for (const item of extractTextItems(result)) {
200
+ const parsed = parseJsonPayload(item.text);
201
+ if (parsed !== null) {
202
+ return parsed;
203
+ }
204
+ }
205
+ return null;
206
+ }
207
+ function pickResourceArray(payload) {
208
+ if (Array.isArray(payload)) {
209
+ return payload;
210
+ }
211
+ if (!isRecord(payload)) {
212
+ return null;
213
+ }
214
+ for (const key of RESOURCE_ARRAY_KEYS) {
215
+ const value = payload[key];
216
+ if (Array.isArray(value)) {
217
+ return value;
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+ function toStringValue(value) {
223
+ if (typeof value === "string") {
224
+ return value;
225
+ }
226
+ if (typeof value === "number" || typeof value === "boolean") {
227
+ return String(value);
228
+ }
229
+ return null;
230
+ }
231
+ function normalizeResource(raw) {
232
+ if (!isRecord(raw)) {
233
+ return null;
234
+ }
235
+ const cloudId = toStringValue(raw.id) ??
236
+ toStringValue(raw.cloudId) ??
237
+ toStringValue(raw.cloud_id) ??
238
+ toStringValue(raw.resourceId) ??
239
+ toStringValue(raw.resource_id);
240
+ const url = toStringValue(raw.url) ??
241
+ toStringValue(raw.baseUrl) ??
242
+ toStringValue(raw.base_url) ??
243
+ toStringValue(raw.siteUrl);
244
+ const name = toStringValue(raw.name) ??
245
+ toStringValue(raw.label) ??
246
+ toStringValue(raw.displayName);
247
+ if (!(cloudId && url)) {
248
+ return null;
249
+ }
250
+ return {
251
+ id: cloudId,
252
+ url,
253
+ name: name ?? url,
254
+ };
255
+ }
256
+ function dedupeResources(resources) {
257
+ const unique = new Map();
258
+ for (const item of resources) {
259
+ const key = `${item.id}|${item.url}`;
260
+ if (!unique.has(key)) {
261
+ unique.set(key, item);
262
+ }
263
+ }
264
+ return Array.from(unique.values());
265
+ }
266
+ function toolNameSet(tools) {
267
+ return new Set(tools.map((tool) => tool.name));
268
+ }
269
+ async function fetchAccessibleResources(session, toolNames) {
270
+ if (!toolNames.has("getAccessibleAtlassianResources")) {
271
+ warn("MCP tool getAccessibleAtlassianResources not available. Storing account without site metadata.");
272
+ return [];
273
+ }
274
+ const result = await session.callTool("getAccessibleAtlassianResources", {});
275
+ const payload = extractStructuredResult(result);
276
+ const resources = pickResourceArray(payload) ?? [];
277
+ const normalized = resources
278
+ .map(normalizeResource)
279
+ .filter((item) => Boolean(item));
280
+ return dedupeResources(normalized);
281
+ }
282
+ async function selectResource(resources) {
283
+ if (resources.length === 0) {
284
+ return null;
285
+ }
286
+ if (resources.length === 1) {
287
+ return resources[0];
288
+ }
289
+ const selected = await select({
290
+ message: "Select the Jira site to link:",
291
+ choices: resources.map((item) => ({
292
+ name: `${item.name} (${item.url})`,
293
+ value: item.id,
294
+ })),
295
+ });
296
+ return resources.find((item) => item.id === selected) ?? resources[0];
297
+ }
298
+ function extractUserEmail(payload) {
299
+ if (!isRecord(payload)) {
300
+ return;
301
+ }
302
+ return (toStringValue(payload.email) ??
303
+ toStringValue(payload.emailAddress) ??
304
+ toStringValue(payload.userEmail) ??
305
+ toStringValue(payload.username) ??
306
+ undefined);
307
+ }
308
+ async function fetchUserEmail(session, toolNames) {
309
+ if (!toolNames.has("atlassianUserInfo")) {
310
+ return;
311
+ }
312
+ try {
313
+ const result = await session.callTool("atlassianUserInfo", {});
314
+ const payload = extractStructuredResult(result);
315
+ return extractUserEmail(payload);
316
+ }
317
+ catch {
318
+ return;
319
+ }
320
+ }
321
+ async function handleLogin(alias, options) {
322
+ const config = await loadConfig();
323
+ if (config.accounts[alias]) {
324
+ const overwrite = await confirm({
325
+ message: `Account alias "${alias}" already exists. Re-authenticate and overwrite?`,
326
+ default: false,
327
+ });
328
+ if (!overwrite) {
329
+ info("Login cancelled.");
330
+ return;
331
+ }
332
+ }
333
+ const scopes = resolveScopes(options.scopes);
334
+ const tokenStoreKind = resolveTokenStoreFromConfig(config);
335
+ const tokenStore = await createTokenStore({
336
+ store: tokenStoreKind,
337
+ });
338
+ const staticClientInfo = getStaticClientInfoFromEnv(options);
339
+ await loginWithDynamicOAuth({
340
+ alias,
341
+ tokenStore,
342
+ scopes,
343
+ staticClientInfo,
344
+ });
345
+ const tokens = await tokenStore.get(alias);
346
+ if (!tokens) {
347
+ throw new Error("Login failed: no tokens stored.");
348
+ }
349
+ const tempAccount = {
350
+ alias,
351
+ site: "",
352
+ cloudId: "",
353
+ };
354
+ const session = new RemoteSession(tempAccount, tokenStore, scopes, staticClientInfo);
355
+ let resource = null;
356
+ let user;
357
+ try {
358
+ const tools = await session.listTools();
359
+ const toolNames = toolNameSet(tools);
360
+ const resources = await fetchAccessibleResources(session, toolNames);
361
+ if (resources.length === 0) {
362
+ warn("No accessible Jira resources found via MCP.");
363
+ }
364
+ resource = await selectResource(resources);
365
+ user = await fetchUserEmail(session, toolNames);
366
+ }
367
+ finally {
368
+ await session.close();
369
+ }
370
+ const account = {
371
+ alias,
372
+ site: resource?.url ?? "unknown",
373
+ cloudId: resource?.id ?? "unknown",
374
+ user,
375
+ default: options.default ?? Object.keys(config.accounts).length === 0,
376
+ };
377
+ await setAccount(account);
378
+ info(`Account "${alias}" connected to ${account.site}.`);
379
+ }
380
+ async function handleListAccounts() {
381
+ const config = await loadConfig();
382
+ const accounts = Object.values(config.accounts);
383
+ if (accounts.length === 0) {
384
+ info(formatAccounts(accounts, new Map()));
385
+ return;
386
+ }
387
+ const storeKind = resolveTokenStoreFromConfig(config);
388
+ const tokenStore = await createTokenStore({ store: storeKind });
389
+ const statusMap = new Map();
390
+ for (const account of accounts) {
391
+ try {
392
+ const status = await getAuthStatusForAlias({
393
+ alias: account.alias,
394
+ tokenStore,
395
+ storeKind,
396
+ allowPrompt: process.stdin.isTTY,
397
+ });
398
+ statusMap.set(account.alias, formatAuthStatus(status));
399
+ }
400
+ catch (err) {
401
+ statusMap.set(account.alias, "unknown");
402
+ warn(`Failed to resolve auth status for ${account.alias}: ${String(err)}`);
403
+ }
404
+ }
405
+ info(formatAccounts(accounts, statusMap));
406
+ }
407
+ async function handleRemove(alias) {
408
+ const config = await loadConfig();
409
+ if (!config.accounts[alias]) {
410
+ warn(`No account found for alias "${alias}".`);
411
+ return;
412
+ }
413
+ const confirmed = await confirm({
414
+ message: `Remove account "${alias}" and delete stored tokens?`,
415
+ default: false,
416
+ });
417
+ if (!confirmed) {
418
+ info("Remove cancelled.");
419
+ return;
420
+ }
421
+ const tokenStore = await createTokenStore({
422
+ store: resolveTokenStoreFromConfig(config),
423
+ });
424
+ await tokenStore.remove(alias);
425
+ await removeAccount(alias);
426
+ info(`Removed account "${alias}".`);
427
+ }
428
+ async function handleServe(options) {
429
+ setLogTarget("stderr");
430
+ const config = await loadConfig();
431
+ const scopes = resolveScopes(options.scopes);
432
+ const tokenStoreKind = resolveTokenStoreFromConfig(config);
433
+ const tokenStore = await createTokenStore({
434
+ store: tokenStoreKind,
435
+ });
436
+ const manager = new SessionManager(tokenStore, scopes, getStaticClientInfoFromEnv(options), tokenStoreKind);
437
+ await manager.loadAll();
438
+ if (manager.listAccounts().length === 0) {
439
+ warn("No accounts configured. Run `mcp-multi-jira login <alias>` first.");
440
+ return;
441
+ }
442
+ await manager.connectAll();
443
+ await startLocalServer(manager, PACKAGE_VERSION);
444
+ }
445
+ function warnTokenStoreOverride(config) {
446
+ const envOverride = normalizeTokenStore(process.env.MCP_JIRA_TOKEN_STORE);
447
+ if (envOverride && envOverride !== config.tokenStore) {
448
+ warn(`MCP_JIRA_TOKEN_STORE is set to ${envOverride}. This overrides the configured default (${config.tokenStore ?? "plain"}).`);
449
+ }
450
+ }
451
+ async function showTokenStoreStatus() {
452
+ const config = await loadConfig();
453
+ warnTokenStoreOverride(config);
454
+ const effective = resolveTokenStoreFromConfig(config);
455
+ info(`Current token store: ${effective}.`);
456
+ info("Available token stores: encrypted, plain, keychain.");
457
+ info("Set with: mcp-multi-jira token-store <store>");
458
+ }
459
+ async function migrateTokenStoreIfConfirmed(fromStore, toStore, aliases) {
460
+ if (aliases.length === 0) {
461
+ return false;
462
+ }
463
+ if (!process.stdin.isTTY) {
464
+ warn("Accounts exist but no TTY available to prompt for migration. Tokens will remain in the previous store.");
465
+ return false;
466
+ }
467
+ const shouldMigrate = await confirm({
468
+ message: `Migrate ${aliases.length} account token(s) from ${describeTokenStore(fromStore)} to ${describeTokenStore(toStore)}? This will move tokens to the new backend.`,
469
+ default: true,
470
+ });
471
+ if (!shouldMigrate) {
472
+ return false;
473
+ }
474
+ const result = await migrateTokenStore({
475
+ from: fromStore,
476
+ to: toStore,
477
+ aliases,
478
+ });
479
+ info(`Migrated ${result.migrated} account(s) to ${toStore}.`);
480
+ if (result.alreadyPresent > 0) {
481
+ info(`${result.alreadyPresent} account(s) already had tokens in ${toStore}.`);
482
+ }
483
+ if (result.missing > 0) {
484
+ warn(`${result.missing} account(s) had no tokens in the previous store.`);
485
+ }
486
+ return true;
487
+ }
488
+ async function setTokenStoreWithMigration(store) {
489
+ const config = await loadConfig();
490
+ warnTokenStoreOverride(config);
491
+ const currentStore = config.tokenStore ?? "plain";
492
+ if (currentStore === store) {
493
+ info(`Token store already set to ${store}.`);
494
+ return;
495
+ }
496
+ const aliases = Object.keys(config.accounts);
497
+ const migrated = await migrateTokenStoreIfConfirmed(currentStore, store, aliases);
498
+ await setTokenStore(store);
499
+ info(`Default token store set to ${store}.`);
500
+ if (!migrated && aliases.length > 0) {
501
+ warn("Tokens remain in the previous store. Run the token-store command again to migrate, or re-login.");
502
+ }
503
+ }
504
+ async function handleTokenStore(storeValue) {
505
+ if (!storeValue) {
506
+ await showTokenStoreStatus();
507
+ return;
508
+ }
509
+ const normalized = normalizeTokenStore(storeValue);
510
+ if (!normalized) {
511
+ throw new Error("Invalid token store. Use one of: encrypted, plain, keychain.");
512
+ }
513
+ await setTokenStoreWithMigration(normalized);
514
+ }
515
+ async function main() {
516
+ const program = new Command();
517
+ program
518
+ .name("mcp-multi-jira")
519
+ .description("Multi-account Jira MCP server and CLI")
520
+ .version(PACKAGE_VERSION)
521
+ .option("--client-id <clientId>", "Atlassian OAuth client ID")
522
+ .option("--client-secret <clientSecret>", "Atlassian OAuth client secret")
523
+ .option("--scopes <scopes>", "OAuth scopes (space or comma separated)");
524
+ program
525
+ .command("login")
526
+ .argument("<alias>", "Account alias to store")
527
+ .option("--default", "Mark this account as default")
528
+ .action(async (alias, options) => {
529
+ const opts = program.opts();
530
+ await handleLogin(alias, { ...opts, ...options });
531
+ });
532
+ program
533
+ .command("list")
534
+ .description("List configured Jira accounts")
535
+ .action(handleListAccounts);
536
+ program
537
+ .command("remove")
538
+ .argument("<alias>", "Account alias to remove")
539
+ .description("Remove a Jira account and delete stored tokens")
540
+ .action(async (alias) => {
541
+ await handleRemove(alias);
542
+ });
543
+ program
544
+ .command("serve")
545
+ .description("Start the local MCP server")
546
+ .action(async (options) => {
547
+ const opts = program.opts();
548
+ await handleServe({ ...opts, ...options });
549
+ });
550
+ program
551
+ .command("token-store")
552
+ .argument("[store]", "Token store backend (encrypted|plain|keychain)")
553
+ .description("Set the default token storage backend")
554
+ .action(async (store) => {
555
+ await handleTokenStore(store);
556
+ });
557
+ program
558
+ .command("install")
559
+ .description("Install MCP configuration into supported agents")
560
+ .action(async () => {
561
+ const config = await loadConfig();
562
+ await runInstaller({
563
+ tokenStore: normalizeTokenStore(process.env.MCP_JIRA_TOKEN_STORE) ??
564
+ config.tokenStore,
565
+ });
566
+ });
567
+ if (process.argv.length <= 2) {
568
+ program.outputHelp();
569
+ return;
570
+ }
571
+ await program.parseAsync(process.argv);
572
+ }
573
+ main().catch((err) => {
574
+ console.error(err instanceof Error ? err.message : err);
575
+ process.exitCode = 1;
576
+ });
@@ -0,0 +1,14 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export function configDir() {
4
+ return path.join(os.homedir(), ".mcp-jira");
5
+ }
6
+ export function configFilePath() {
7
+ return path.join(configDir(), "config.json");
8
+ }
9
+ export function tokenFilePath() {
10
+ return path.join(configDir(), "tokens.enc.json");
11
+ }
12
+ export function plainTokenFilePath() {
13
+ return path.join(configDir(), "tokens.json");
14
+ }
@@ -0,0 +1,54 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { atomicWrite, ensureDir } from "../utils/fs.js";
3
+ import { configDir, configFilePath } from "./paths.js";
4
+ const emptyConfig = { accounts: {} };
5
+ export async function loadConfig() {
6
+ const filePath = configFilePath();
7
+ try {
8
+ const raw = await fs.readFile(filePath, "utf8");
9
+ const parsed = JSON.parse(raw);
10
+ if (!parsed.accounts) {
11
+ return { ...emptyConfig };
12
+ }
13
+ return parsed;
14
+ }
15
+ catch (err) {
16
+ if (err.code === "ENOENT") {
17
+ return { ...emptyConfig };
18
+ }
19
+ throw err;
20
+ }
21
+ }
22
+ export async function saveConfig(config) {
23
+ await ensureDir(configDir());
24
+ await atomicWrite(configFilePath(), JSON.stringify(config, null, 2));
25
+ }
26
+ export async function setAccount(account) {
27
+ const config = await loadConfig();
28
+ config.accounts[account.alias] = account;
29
+ await saveConfig(config);
30
+ }
31
+ export async function removeAccount(alias) {
32
+ const config = await loadConfig();
33
+ if (config.accounts[alias]) {
34
+ delete config.accounts[alias];
35
+ await saveConfig(config);
36
+ }
37
+ }
38
+ export async function getAccount(alias) {
39
+ const config = await loadConfig();
40
+ return config.accounts[alias] ?? null;
41
+ }
42
+ export async function listAccounts() {
43
+ const config = await loadConfig();
44
+ return Object.values(config.accounts);
45
+ }
46
+ export async function getTokenStore() {
47
+ const config = await loadConfig();
48
+ return config.tokenStore ?? null;
49
+ }
50
+ export async function setTokenStore(tokenStore) {
51
+ const config = await loadConfig();
52
+ config.tokenStore = tokenStore;
53
+ await saveConfig(config);
54
+ }
Binary file
@@ -0,0 +1,63 @@
1
+ const account = {
2
+ alias: "mock",
3
+ site: "mock://jira",
4
+ cloudId: "mock",
5
+ };
6
+ const tools = [
7
+ {
8
+ name: "mockEcho",
9
+ description: "Echoes arguments back as JSON.",
10
+ inputSchema: {
11
+ type: "object",
12
+ properties: {
13
+ cloudId: { type: "string" },
14
+ jql: { type: "string" },
15
+ },
16
+ required: ["cloudId", "jql"],
17
+ },
18
+ },
19
+ {
20
+ name: "mockSecondTool",
21
+ description: "Second tool for pass-through tests.",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ cloudId: { type: "string" },
26
+ query: { type: "string" },
27
+ },
28
+ required: ["cloudId", "query"],
29
+ },
30
+ },
31
+ ];
32
+ const session = {
33
+ async listTools() {
34
+ return tools;
35
+ },
36
+ async callTool(_name, args) {
37
+ return {
38
+ content: [
39
+ {
40
+ type: "text",
41
+ text: JSON.stringify(args),
42
+ },
43
+ ],
44
+ structuredContent: args,
45
+ };
46
+ },
47
+ };
48
+ export function createMockSessionManager() {
49
+ return {
50
+ listAccounts() {
51
+ return [account];
52
+ },
53
+ getSession(alias) {
54
+ if (alias === account.alias) {
55
+ return session;
56
+ }
57
+ return null;
58
+ },
59
+ async getAccountAuthStatus() {
60
+ return { status: "ok" };
61
+ },
62
+ };
63
+ }