adoptai-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +70 -0
  2. package/bin/adoptai-mcp.js +2 -0
  3. package/dist/apps/canva.js +1 -0
  4. package/dist/apps/figma.js +1 -0
  5. package/dist/apps/github.js +2 -0
  6. package/dist/apps/notion.js +1 -0
  7. package/dist/apps/registry.js +20 -0
  8. package/dist/apps/salesforce.js +1 -0
  9. package/dist/cli/add.js +532 -0
  10. package/dist/cli/index.js +39 -0
  11. package/dist/cli/list.js +19 -0
  12. package/dist/cli/remove.js +37 -0
  13. package/dist/cli/serve.js +27 -0
  14. package/dist/cli/status.js +24 -0
  15. package/dist/config/clients.js +118 -0
  16. package/dist/config/credentials.js +34 -0
  17. package/dist/core/auth-manager.js +237 -0
  18. package/dist/core/config-writer.js +161 -0
  19. package/dist/core/doctor.js +199 -0
  20. package/dist/core/package.json +3 -0
  21. package/dist/core/server-base.js +81 -0
  22. package/dist/integrations/canva/.env +3 -0
  23. package/dist/integrations/canva/auth.js +287 -0
  24. package/dist/integrations/canva/env.js +9 -0
  25. package/dist/integrations/canva/index.js +12 -0
  26. package/dist/integrations/canva/package.json +31 -0
  27. package/dist/integrations/canva/publish-to-adoptai.js +365 -0
  28. package/dist/integrations/canva/setup.js +90 -0
  29. package/dist/integrations/canva/tools.js +1315 -0
  30. package/dist/integrations/canva/tools.original.js +1315 -0
  31. package/dist/integrations/figma/auth.js +48 -0
  32. package/dist/integrations/figma/index.js +11 -0
  33. package/dist/integrations/figma/package.json +27 -0
  34. package/dist/integrations/figma/publish-to-adoptai.js +384 -0
  35. package/dist/integrations/figma/setup.js +90 -0
  36. package/dist/integrations/figma/tools.js +1137 -0
  37. package/dist/integrations/github/auth.js +53 -0
  38. package/dist/integrations/github/index.js +11 -0
  39. package/dist/integrations/github/package.json +28 -0
  40. package/dist/integrations/github/publish-to-adoptai.js +240 -0
  41. package/dist/integrations/github/setup.js +103 -0
  42. package/dist/integrations/github/tools.js +78 -0
  43. package/dist/integrations/github-actions/auth.js +53 -0
  44. package/dist/integrations/github-actions/index.js +11 -0
  45. package/dist/integrations/github-actions/package.json +27 -0
  46. package/dist/integrations/github-actions/setup.js +103 -0
  47. package/dist/integrations/github-actions/tools.js +5642 -0
  48. package/dist/integrations/github-activity/auth.js +53 -0
  49. package/dist/integrations/github-activity/index.js +11 -0
  50. package/dist/integrations/github-activity/package.json +27 -0
  51. package/dist/integrations/github-activity/setup.js +103 -0
  52. package/dist/integrations/github-activity/tools.js +925 -0
  53. package/dist/integrations/github-apps/auth.js +53 -0
  54. package/dist/integrations/github-apps/index.js +11 -0
  55. package/dist/integrations/github-apps/package.json +27 -0
  56. package/dist/integrations/github-apps/setup.js +103 -0
  57. package/dist/integrations/github-apps/tools.js +791 -0
  58. package/dist/integrations/github-billing/auth.js +53 -0
  59. package/dist/integrations/github-billing/index.js +11 -0
  60. package/dist/integrations/github-billing/package.json +27 -0
  61. package/dist/integrations/github-billing/setup.js +103 -0
  62. package/dist/integrations/github-billing/tools.js +438 -0
  63. package/dist/integrations/github-checks/auth.js +53 -0
  64. package/dist/integrations/github-checks/index.js +11 -0
  65. package/dist/integrations/github-checks/package.json +27 -0
  66. package/dist/integrations/github-checks/setup.js +103 -0
  67. package/dist/integrations/github-checks/tools.js +607 -0
  68. package/dist/integrations/github-code-scanning/auth.js +53 -0
  69. package/dist/integrations/github-code-scanning/index.js +11 -0
  70. package/dist/integrations/github-code-scanning/package.json +27 -0
  71. package/dist/integrations/github-code-scanning/setup.js +103 -0
  72. package/dist/integrations/github-code-scanning/tools.js +987 -0
  73. package/dist/integrations/github-dependabot/auth.js +53 -0
  74. package/dist/integrations/github-dependabot/index.js +11 -0
  75. package/dist/integrations/github-dependabot/package.json +27 -0
  76. package/dist/integrations/github-dependabot/setup.js +103 -0
  77. package/dist/integrations/github-dependabot/tools.js +915 -0
  78. package/dist/integrations/github-gists/auth.js +53 -0
  79. package/dist/integrations/github-gists/index.js +11 -0
  80. package/dist/integrations/github-gists/package.json +27 -0
  81. package/dist/integrations/github-gists/setup.js +103 -0
  82. package/dist/integrations/github-gists/tools.js +545 -0
  83. package/dist/integrations/github-git/auth.js +53 -0
  84. package/dist/integrations/github-git/index.js +11 -0
  85. package/dist/integrations/github-git/package.json +27 -0
  86. package/dist/integrations/github-git/setup.js +103 -0
  87. package/dist/integrations/github-git/tools.js +513 -0
  88. package/dist/integrations/github-issues/auth.js +53 -0
  89. package/dist/integrations/github-issues/index.js +11 -0
  90. package/dist/integrations/github-issues/package.json +27 -0
  91. package/dist/integrations/github-issues/setup.js +103 -0
  92. package/dist/integrations/github-issues/tools.js +2232 -0
  93. package/dist/integrations/github-orgs/auth.js +53 -0
  94. package/dist/integrations/github-orgs/index.js +11 -0
  95. package/dist/integrations/github-orgs/package.json +27 -0
  96. package/dist/integrations/github-orgs/setup.js +103 -0
  97. package/dist/integrations/github-orgs/tools.js +3512 -0
  98. package/dist/integrations/github-packages/auth.js +53 -0
  99. package/dist/integrations/github-packages/index.js +11 -0
  100. package/dist/integrations/github-packages/package.json +27 -0
  101. package/dist/integrations/github-packages/setup.js +103 -0
  102. package/dist/integrations/github-packages/tools.js +1088 -0
  103. package/dist/integrations/github-pulls/auth.js +53 -0
  104. package/dist/integrations/github-pulls/index.js +11 -0
  105. package/dist/integrations/github-pulls/package.json +27 -0
  106. package/dist/integrations/github-pulls/setup.js +103 -0
  107. package/dist/integrations/github-pulls/tools.js +1252 -0
  108. package/dist/integrations/github-reactions/auth.js +53 -0
  109. package/dist/integrations/github-reactions/index.js +11 -0
  110. package/dist/integrations/github-reactions/package.json +27 -0
  111. package/dist/integrations/github-reactions/setup.js +103 -0
  112. package/dist/integrations/github-reactions/tools.js +706 -0
  113. package/dist/integrations/github-repos/auth.js +53 -0
  114. package/dist/integrations/github-repos/index.js +11 -0
  115. package/dist/integrations/github-repos/package.json +27 -0
  116. package/dist/integrations/github-repos/setup.js +103 -0
  117. package/dist/integrations/github-repos/tools.js +7286 -0
  118. package/dist/integrations/github-search/auth.js +53 -0
  119. package/dist/integrations/github-search/index.js +11 -0
  120. package/dist/integrations/github-search/package.json +27 -0
  121. package/dist/integrations/github-search/setup.js +103 -0
  122. package/dist/integrations/github-search/tools.js +370 -0
  123. package/dist/integrations/github-teams/auth.js +53 -0
  124. package/dist/integrations/github-teams/index.js +11 -0
  125. package/dist/integrations/github-teams/package.json +27 -0
  126. package/dist/integrations/github-teams/setup.js +103 -0
  127. package/dist/integrations/github-teams/tools.js +633 -0
  128. package/dist/integrations/github-users/auth.js +53 -0
  129. package/dist/integrations/github-users/index.js +11 -0
  130. package/dist/integrations/github-users/package.json +27 -0
  131. package/dist/integrations/github-users/setup.js +103 -0
  132. package/dist/integrations/github-users/tools.js +1118 -0
  133. package/dist/integrations/notion/api.js +108 -0
  134. package/dist/integrations/notion/auth.js +59 -0
  135. package/dist/integrations/notion/endpoints.json +630 -0
  136. package/dist/integrations/notion/index.js +11 -0
  137. package/dist/integrations/notion/package.json +33 -0
  138. package/dist/integrations/notion/publish-to-adoptai.js +271 -0
  139. package/dist/integrations/notion/scripts/generate-endpoints.mjs +306 -0
  140. package/dist/integrations/notion/setup.js +89 -0
  141. package/dist/integrations/notion/tools.js +586 -0
  142. package/dist/integrations/notion/tools.original.js +568 -0
  143. package/dist/integrations/salesforce/.env +8 -0
  144. package/dist/integrations/salesforce/.env.example +15 -0
  145. package/dist/integrations/salesforce/auth.js +311 -0
  146. package/dist/integrations/salesforce/endpoints.json +1359 -0
  147. package/dist/integrations/salesforce/env.js +9 -0
  148. package/dist/integrations/salesforce/index.js +12 -0
  149. package/dist/integrations/salesforce/package.json +42 -0
  150. package/dist/integrations/salesforce/publish-smart-specs.js +890 -0
  151. package/dist/integrations/salesforce/publish-to-adoptai.js +386 -0
  152. package/dist/integrations/salesforce/scripts/extract-postman.mjs +222 -0
  153. package/dist/integrations/salesforce/setup.js +112 -0
  154. package/dist/integrations/salesforce/tools.js +4544 -0
  155. package/dist/integrations/salesforce/tools.original.js +4487 -0
  156. package/dist/server/mcp-server.js +50 -0
  157. package/dist/server/tool-loader.js +47 -0
  158. package/dist/specs/figma-api.json +13621 -0
  159. package/dist/specs/split/salesforce-auth.json +3931 -0
  160. package/dist/specs/split/salesforce-bulk-v1.json +1489 -0
  161. package/dist/specs/split/salesforce-bulk-v2.json +1951 -0
  162. package/dist/specs/split/salesforce-composite.json +1246 -0
  163. package/dist/specs/split/salesforce-connect.json +11639 -0
  164. package/dist/specs/split/salesforce-einstein-prediction-service.json +576 -0
  165. package/dist/specs/split/salesforce-event-platform.json +2682 -0
  166. package/dist/specs/split/salesforce-graphql.json +1754 -0
  167. package/dist/specs/split/salesforce-industries.json +4115 -0
  168. package/dist/specs/split/salesforce-metadata.json +555 -0
  169. package/dist/specs/split/salesforce-rest.json +4798 -0
  170. package/dist/specs/split/salesforce-soap.json +210 -0
  171. package/dist/specs/split/salesforce-subscription-management.json +1299 -0
  172. package/dist/specs/split/salesforce-tooling.json +2026 -0
  173. package/dist/specs/split/salesforce-ui.json +7426 -0
  174. package/package.json +47 -0
@@ -0,0 +1,19 @@
1
+ import chalk from 'chalk';
2
+ import { APP_SLUGS, countToolsForApp } from '../apps/registry.js';
3
+ import { hasAppCredentials } from '../config/credentials.js';
4
+ export async function runList() {
5
+ console.log(chalk.bold('\n Available Apps'));
6
+ console.log(chalk.dim(' ──────────────────────────────────────────'));
7
+ let total = 0;
8
+ for (const app of APP_SLUGS) {
9
+ const n = await countToolsForApp(app);
10
+ total += n;
11
+ const ok = hasAppCredentials(app);
12
+ const mark = ok ? chalk.green('✅') : chalk.red('❌');
13
+ const auth = ok ? chalk.green('authenticated') : chalk.dim('not authenticated');
14
+ console.log(` ${mark} ${app.padEnd(12)} ${String(n).padStart(4)} tools ${auth}`);
15
+ }
16
+ console.log(chalk.dim(' ──────────────────────────────────────────'));
17
+ console.log(chalk.bold(` Total: ${total.toLocaleString()} tools across ${APP_SLUGS.length} apps\n`));
18
+ console.log(chalk.cyan(' Run: npx adoptai-mcp add --app <name> --client cursor\n'));
19
+ }
@@ -0,0 +1,37 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { isAppSlug } from '../apps/registry.js';
4
+ import { getCredentialsStore } from '../config/credentials.js';
5
+ import { isClientId, removeAdoptaiMcpServer } from '../config/clients.js';
6
+ export async function runRemove(argv) {
7
+ const app = argv.app.trim().toLowerCase();
8
+ if (!isAppSlug(app)) {
9
+ console.error(chalk.red(`Unknown app "${argv.app}".`));
10
+ process.exit(1);
11
+ }
12
+ const clientRaw = argv.client;
13
+ if (!isClientId(clientRaw)) {
14
+ console.error(chalk.red(`Unknown client "${clientRaw}".`));
15
+ process.exit(1);
16
+ }
17
+ const client = clientRaw;
18
+ const removed = removeAdoptaiMcpServer(client, app);
19
+ if (!removed) {
20
+ console.log(chalk.yellow(`No ${app}-adoptai entry found for ${client}.`));
21
+ }
22
+ else {
23
+ console.log(chalk.green(`Removed ${app}-adoptai from ${client} config.`));
24
+ }
25
+ const { del } = await inquirer.prompt([
26
+ {
27
+ type: 'confirm',
28
+ name: 'del',
29
+ message: 'Delete saved credentials too?',
30
+ default: false,
31
+ },
32
+ ]);
33
+ if (del) {
34
+ getCredentialsStore().delete(app);
35
+ console.log(chalk.green(`Deleted stored credentials for ${app}.`));
36
+ }
37
+ }
@@ -0,0 +1,27 @@
1
+ import { isAppSlug } from '../apps/registry.js';
2
+ import { getCredentialsStore } from '../config/credentials.js';
3
+ import { applyCredentialsToEnv, loadToolsForApp } from '../server/tool-loader.js';
4
+ import { runMcpServer } from '../server/mcp-server.js';
5
+ function pickCredentials(app, store) {
6
+ const raw = store.get(app);
7
+ if (!raw || typeof raw !== 'object')
8
+ return null;
9
+ return raw;
10
+ }
11
+ export async function runServe(argv) {
12
+ const app = argv.app;
13
+ if (!isAppSlug(app)) {
14
+ console.error(`Unknown app "${app}".`);
15
+ process.exit(1);
16
+ }
17
+ const store = getCredentialsStore();
18
+ const creds = pickCredentials(app, store);
19
+ if (!creds) {
20
+ console.error(`No saved credentials for "${app}". Run:\n npx adoptai-mcp add --app ${app} --client cursor`);
21
+ process.exit(1);
22
+ }
23
+ applyCredentialsToEnv(app, creds);
24
+ const tools = await loadToolsForApp(app);
25
+ const serverName = `${app}-adoptai`;
26
+ await runMcpServer({ name: serverName, version: '1.0.0', tools });
27
+ }
@@ -0,0 +1,24 @@
1
+ import chalk from 'chalk';
2
+ import { APP_SLUGS, countToolsForApp } from '../apps/registry.js';
3
+ import { hasAppCredentials } from '../config/credentials.js';
4
+ import { CLIENT_IDS, clientConfigPath, listAdoptaiAppsInClient } from '../config/clients.js';
5
+ export async function runStatus() {
6
+ console.log(chalk.bold('\n Clients'));
7
+ console.log(chalk.dim(' ──────────────────────────────────────────'));
8
+ for (const c of CLIENT_IDS) {
9
+ const apps = listAdoptaiAppsInClient(c);
10
+ const p = clientConfigPath(c);
11
+ const line = apps.length ? apps.join(', ') : chalk.dim('(none)');
12
+ console.log(` ${chalk.bold(c)}: ${line}`);
13
+ if (p)
14
+ console.log(chalk.dim(` ${p}`));
15
+ }
16
+ const connected = APP_SLUGS.filter((a) => hasAppCredentials(a));
17
+ let toolTotal = 0;
18
+ for (const a of connected) {
19
+ toolTotal += await countToolsForApp(a);
20
+ }
21
+ console.log(chalk.bold('\n Stored credentials (connected apps)'));
22
+ console.log(` ${connected.length ? connected.join(', ') : chalk.dim('none')}`);
23
+ console.log(chalk.bold(`\n Total tools (authenticated apps): ${toolTotal.toLocaleString()}\n`));
24
+ }
@@ -0,0 +1,118 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ export const CLIENT_IDS = ['cursor', 'claude', 'windsurf', 'vscode'];
6
+ export function clientConfigPath(client) {
7
+ switch (client) {
8
+ case 'cursor':
9
+ return join(homedir(), '.cursor', 'mcp.json');
10
+ case 'claude':
11
+ return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
12
+ case 'windsurf':
13
+ return join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
14
+ case 'vscode':
15
+ return null;
16
+ default:
17
+ return null;
18
+ }
19
+ }
20
+ export function adoptaiServerKey(app) {
21
+ return `${app}-adoptai`;
22
+ }
23
+ export function mcpServerEntryForApp(app) {
24
+ return {
25
+ command: 'npx',
26
+ args: ['-y', 'adoptai-mcp', 'serve', '--app', app],
27
+ env: {},
28
+ };
29
+ }
30
+ export function readClientConfigFile(client) {
31
+ const p = clientConfigPath(client);
32
+ if (!p || !existsSync(p))
33
+ return {};
34
+ try {
35
+ return JSON.parse(readFileSync(p, 'utf8'));
36
+ }
37
+ catch {
38
+ return {};
39
+ }
40
+ }
41
+ export function readMcpServers(client) {
42
+ const j = readClientConfigFile(client);
43
+ const mcp = j.mcpServers;
44
+ if (mcp && typeof mcp === 'object' && !Array.isArray(mcp)) {
45
+ return mcp;
46
+ }
47
+ return {};
48
+ }
49
+ function writeClientConfigRaw(client, data) {
50
+ const p = clientConfigPath(client);
51
+ if (!p) {
52
+ throw new Error('No config path for this client');
53
+ }
54
+ const dir = dirname(p);
55
+ if (!existsSync(dir)) {
56
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
57
+ }
58
+ writeFileSync(p, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
59
+ }
60
+ export function upsertAdoptaiMcpServer(client, app) {
61
+ if (client === 'vscode') {
62
+ const entry = mcpServerEntryForApp(app);
63
+ const r = spawnSync('code', ['--add-mcp', adoptaiServerKey(app), entry.command, ...entry.args], {
64
+ encoding: 'utf8',
65
+ });
66
+ if (r.error || (r.status !== 0 && r.status !== null)) {
67
+ throw new Error('Could not run `code --add-mcp`. Add this server manually in VS Code MCP settings:\n' +
68
+ JSON.stringify({ [adoptaiServerKey(app)]: entry }, null, 2));
69
+ }
70
+ return 'VS Code (via code --add-mcp)';
71
+ }
72
+ const base = readClientConfigFile(client);
73
+ const mcpServers = { ...readMcpServers(client) };
74
+ mcpServers[adoptaiServerKey(app)] = mcpServerEntryForApp(app);
75
+ base.mcpServers = mcpServers;
76
+ writeClientConfigRaw(client, base);
77
+ return clientConfigPath(client) ?? '';
78
+ }
79
+ export function removeAdoptaiMcpServer(client, app) {
80
+ if (client === 'vscode') {
81
+ spawnSync('code', ['--remove-mcp', adoptaiServerKey(app)], { encoding: 'utf8' });
82
+ return true;
83
+ }
84
+ const p = clientConfigPath(client);
85
+ if (!p || !existsSync(p))
86
+ return false;
87
+ const base = readClientConfigFile(client);
88
+ const mcpServers = { ...readMcpServers(client) };
89
+ const key = adoptaiServerKey(app);
90
+ if (!mcpServers[key])
91
+ return false;
92
+ delete mcpServers[key];
93
+ base.mcpServers = mcpServers;
94
+ writeClientConfigRaw(client, base);
95
+ return true;
96
+ }
97
+ export function listAdoptaiAppsInClient(client) {
98
+ const servers = readMcpServers(client);
99
+ const apps = [];
100
+ for (const name of Object.keys(servers)) {
101
+ if (!name.endsWith('-adoptai'))
102
+ continue;
103
+ const entry = servers[name];
104
+ if (!entry?.args || !Array.isArray(entry.args))
105
+ continue;
106
+ const i = entry.args.indexOf('--app');
107
+ if (i >= 0 && entry.args[i + 1]) {
108
+ apps.push(String(entry.args[i + 1]));
109
+ }
110
+ else {
111
+ apps.push(name.replace(/-adoptai$/, ''));
112
+ }
113
+ }
114
+ return [...new Set(apps)];
115
+ }
116
+ export function isClientId(s) {
117
+ return CLIENT_IDS.includes(s);
118
+ }
@@ -0,0 +1,34 @@
1
+ import Conf from 'conf';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ const ADOPTAI_DIR = join(homedir(), '.adoptai');
7
+ function getOrCreateEncryptionKey() {
8
+ if (!existsSync(ADOPTAI_DIR)) {
9
+ mkdirSync(ADOPTAI_DIR, { recursive: true, mode: 0o700 });
10
+ }
11
+ const keyPath = join(ADOPTAI_DIR, '.encryption-key');
12
+ if (existsSync(keyPath)) {
13
+ return readFileSync(keyPath, 'utf8').trim();
14
+ }
15
+ const key = randomBytes(32).toString('hex');
16
+ writeFileSync(keyPath, key, { mode: 0o600 });
17
+ return key;
18
+ }
19
+ let _store = null;
20
+ export function getCredentialsStore() {
21
+ if (!_store) {
22
+ _store = new Conf({
23
+ projectName: 'adoptai-mcp',
24
+ cwd: ADOPTAI_DIR,
25
+ configName: 'credentials',
26
+ encryptionKey: getOrCreateEncryptionKey(),
27
+ });
28
+ }
29
+ return _store;
30
+ }
31
+ export function hasAppCredentials(app) {
32
+ const store = getCredentialsStore();
33
+ return store.has(String(app));
34
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @file auth-manager.js
3
+ * Unified token storage, retrieval, expiry detection, and refresh for adopt.ai MCP integrations.
4
+ * Storage: ~/.adoptai/<appId>_creds.json
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ const ADOPTAI_DIR = path.join(os.homedir(), '.adoptai');
12
+ const CREDS_SUFFIX = '_creds.json';
13
+
14
+ /**
15
+ * Ensure ~/.adoptai directory exists.
16
+ * @returns {string} Path to .adoptai directory
17
+ */
18
+ function ensureAdoptaiDir() {
19
+ if (!fs.existsSync(ADOPTAI_DIR)) {
20
+ fs.mkdirSync(ADOPTAI_DIR, { recursive: true, mode: 0o700 });
21
+ }
22
+ return ADOPTAI_DIR;
23
+ }
24
+
25
+ /**
26
+ * Get path to credentials file for an app.
27
+ * @param {string} appId - Application id (e.g. 'github', 'slack')
28
+ * @returns {string} Full path to creds file
29
+ */
30
+ function getCredsPath(appId) {
31
+ ensureAdoptaiDir();
32
+ return path.join(ADOPTAI_DIR, `${appId}${CREDS_SUFFIX}`);
33
+ }
34
+
35
+ /**
36
+ * Save credentials for an app.
37
+ * @param {string} appId - Application id
38
+ * @param {object} credObject - Credentials to store
39
+ * @param {string} [credObject.token] - Access token
40
+ * @param {string} [credObject.tokenType] - e.g. 'bearer'
41
+ * @param {string|null} [credObject.expiresAt] - ISO date or null if never expires
42
+ * @param {string|null} [credObject.refreshToken] - Refresh token if applicable
43
+ * @param {string[]} [credObject.scopes] - OAuth scopes
44
+ * @param {string} [credObject.endUserId] - 6sense end_user_id
45
+ * @param {object} [credObject.extra] - Any other keys (e.g. id_token)
46
+ */
47
+ function saveCredentials(appId, credObject) {
48
+ if (process.env.ADOPTAI_MCP_SERVE === '1') {
49
+ const prev = memoryCreds.get(appId) || syntheticCredentialsFromEnv(appId) || {};
50
+ memoryCreds.set(appId, { ...prev, ...credObject, appId });
51
+ return;
52
+ }
53
+
54
+ const credsPath = getCredsPath(appId);
55
+ const payload = {
56
+ appId,
57
+ token: credObject.token ?? null,
58
+ tokenType: credObject.tokenType ?? 'bearer',
59
+ expiresAt: credObject.expiresAt ?? null,
60
+ refreshToken: credObject.refreshToken ?? null,
61
+ scopes: credObject.scopes ?? [],
62
+ endUserId: credObject.endUserId ?? null,
63
+ savedAt: new Date().toISOString(),
64
+ ...credObject,
65
+ };
66
+ fs.writeFileSync(credsPath, JSON.stringify(payload, null, 2), { mode: 0o600 });
67
+ }
68
+
69
+ /** In-memory credentials when adoptai-mcp serve runs (refresh / session updates). */
70
+ const memoryCreds = new Map();
71
+
72
+ function trimEnv(v) {
73
+ if (typeof v !== 'string') return '';
74
+ return v.trim();
75
+ }
76
+
77
+ /**
78
+ * When ADOPTAI_MCP_SERVE=1, prefer env-injected tokens (from ~/.adoptai credentials store)
79
+ * so all integration auth.js modules work without per-app cred files.
80
+ * @param {string} appId
81
+ * @returns {object|null}
82
+ */
83
+ function syntheticCredentialsFromEnv(appId) {
84
+ if (process.env.ADOPTAI_MCP_SERVE !== '1') return null;
85
+
86
+ if (appId === 'github' || appId.startsWith('github-')) {
87
+ const token = trimEnv(process.env.GITHUB_TOKEN);
88
+ if (!token) return null;
89
+ return {
90
+ appId,
91
+ token,
92
+ tokenType: 'bearer',
93
+ expiresAt: null,
94
+ savedAt: new Date().toISOString(),
95
+ };
96
+ }
97
+
98
+ if (appId === 'notion') {
99
+ const token = trimEnv(process.env.NOTION_TOKEN);
100
+ if (!token) return null;
101
+ return {
102
+ appId,
103
+ token,
104
+ tokenType: 'bearer',
105
+ expiresAt: null,
106
+ savedAt: new Date().toISOString(),
107
+ };
108
+ }
109
+
110
+ if (appId === 'figma') {
111
+ const token = trimEnv(process.env.FIGMA_TOKEN);
112
+ if (!token) return null;
113
+ return {
114
+ appId,
115
+ token,
116
+ tokenType: 'bearer',
117
+ expiresAt: null,
118
+ savedAt: new Date().toISOString(),
119
+ };
120
+ }
121
+
122
+ if (appId === 'salesforce') {
123
+ const token = trimEnv(process.env.SALESFORCE_ACCESS_TOKEN);
124
+ if (!token) return null;
125
+ const instanceUrl = trimEnv(process.env.SALESFORCE_INSTANCE_URL);
126
+ const exp = trimEnv(process.env.SALESFORCE_EXPIRES_AT);
127
+ return {
128
+ appId,
129
+ token,
130
+ tokenType: 'bearer',
131
+ instanceUrl: instanceUrl || null,
132
+ refreshToken: trimEnv(process.env.SALESFORCE_REFRESH_TOKEN) || null,
133
+ expiresAt: exp || null,
134
+ savedAt: new Date().toISOString(),
135
+ };
136
+ }
137
+
138
+ if (appId === 'canva') {
139
+ const token = trimEnv(process.env.CANVA_ACCESS_TOKEN);
140
+ if (!token) return null;
141
+ const exp = trimEnv(process.env.CANVA_EXPIRES_AT);
142
+ return {
143
+ appId,
144
+ token,
145
+ tokenType: 'bearer',
146
+ refreshToken: trimEnv(process.env.CANVA_REFRESH_TOKEN) || null,
147
+ expiresAt: exp || null,
148
+ scopes: [],
149
+ savedAt: new Date().toISOString(),
150
+ };
151
+ }
152
+
153
+ return null;
154
+ }
155
+
156
+ /**
157
+ * Get credentials for an app.
158
+ * @param {string} appId - Application id
159
+ * @returns {object|null} Stored credentials or null if not found
160
+ */
161
+ function getCredentials(appId) {
162
+ if (process.env.ADOPTAI_MCP_SERVE === '1') {
163
+ if (memoryCreds.has(appId)) return memoryCreds.get(appId);
164
+ const syn = syntheticCredentialsFromEnv(appId);
165
+ if (syn) {
166
+ memoryCreds.set(appId, syn);
167
+ return syn;
168
+ }
169
+ }
170
+
171
+ const credsPath = getCredsPath(appId);
172
+ if (!fs.existsSync(credsPath)) return null;
173
+ try {
174
+ const raw = fs.readFileSync(credsPath, 'utf8');
175
+ return JSON.parse(raw);
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Check if the token for an app is expired.
183
+ * @param {string} appId - Application id
184
+ * @returns {boolean} True if expired or missing, false if valid
185
+ */
186
+ function isTokenExpired(appId) {
187
+ const creds = getCredentials(appId);
188
+ if (!creds || !creds.token) return true;
189
+ if (creds.expiresAt == null) return false;
190
+ try {
191
+ const expiresAt = new Date(creds.expiresAt).getTime();
192
+ return Date.now() >= expiresAt;
193
+ } catch {
194
+ return true;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Clear stored credentials for an app.
200
+ * @param {string} appId - Application id
201
+ */
202
+ function clearCredentials(appId) {
203
+ if (process.env.ADOPTAI_MCP_SERVE === '1') {
204
+ memoryCreds.delete(appId);
205
+ }
206
+ const credsPath = getCredsPath(appId);
207
+ if (fs.existsSync(credsPath)) {
208
+ fs.unlinkSync(credsPath);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * List all adopt.ai credential files and their status.
214
+ * @returns {Array<{appId: string, hasToken: boolean, expired: boolean}>}
215
+ */
216
+ function listAllCredentials() {
217
+ ensureAdoptaiDir();
218
+ const files = fs.readdirSync(ADOPTAI_DIR).filter((f) => f.endsWith(CREDS_SUFFIX));
219
+ return files.map((f) => {
220
+ const appId = f.replace(CREDS_SUFFIX, '');
221
+ const creds = getCredentials(appId);
222
+ const hasToken = !!(creds && creds.token);
223
+ const expired = isTokenExpired(appId);
224
+ return { appId, hasToken, expired };
225
+ });
226
+ }
227
+
228
+ module.exports = {
229
+ ADOPTAI_DIR,
230
+ ensureAdoptaiDir,
231
+ getCredsPath,
232
+ saveCredentials,
233
+ getCredentials,
234
+ isTokenExpired,
235
+ clearCredentials,
236
+ listAllCredentials,
237
+ };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @file config-writer.js
3
+ * Writes MCP server config to supported AI clients (Cursor, Claude, Windsurf, VS Code).
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const SUPPORTED_CLIENTS = ['cursor', 'claude', 'claude-code', 'windsurf', 'vscode'];
11
+
12
+ /**
13
+ * Get config file path for a client. Uses platform-specific paths.
14
+ * @param {string} client - One of: cursor, claude, claude-code, windsurf, vscode
15
+ * @returns {{ path: string, isMcpJson: boolean }} Path; isMcpJson true means we read/write the `mcpServers` object (Cursor, Claude Desktop, Claude Code, etc.)
16
+ */
17
+ function getConfigPath(client) {
18
+ const home = os.homedir();
19
+ const normalized = String(client).toLowerCase().trim();
20
+ if (normalized === 'cursor') {
21
+ return { path: path.join(home, '.cursor', 'mcp.json'), isMcpJson: true };
22
+ }
23
+ if (normalized === 'claude-code') {
24
+ return { path: path.join(home, '.claude.json'), isMcpJson: true };
25
+ }
26
+ if (normalized === 'claude') {
27
+ const base = process.platform === 'win32'
28
+ ? path.join(process.env.APPDATA || home, 'Claude')
29
+ : path.join(home, 'Library', 'Application Support', 'Claude');
30
+ // Claude Desktop expects camelCase `mcpServers` (same as Cursor). Legacy snake_case `mcp_servers` is ignored by the app.
31
+ return { path: path.join(base, 'claude_desktop_config.json'), isMcpJson: true };
32
+ }
33
+ if (normalized === 'windsurf') {
34
+ return { path: path.join(home, '.windsurf', 'mcp.json'), isMcpJson: true };
35
+ }
36
+ if (normalized === 'vscode') {
37
+ return { path: path.join(home, '.vscode', 'mcp.json'), isMcpJson: true };
38
+ }
39
+ return { path: '', isMcpJson: false };
40
+ }
41
+
42
+ /**
43
+ * Read existing config or return default structure.
44
+ * @param {string} configPath - Full path to config file
45
+ * @param {boolean} isMcpJson - True: use `mcpServers`. False: legacy `mcp_servers` only (unused for current clients).
46
+ * @returns {object} Config object
47
+ */
48
+ function readConfig(configPath, isMcpJson) {
49
+ if (!fs.existsSync(configPath)) {
50
+ return isMcpJson ? { mcpServers: {} } : { mcp_servers: {} };
51
+ }
52
+ try {
53
+ const raw = fs.readFileSync(configPath, 'utf8');
54
+ const data = JSON.parse(raw);
55
+ if (isMcpJson) {
56
+ if (!data.mcpServers) data.mcpServers = {};
57
+ const legacy = data.mcp_servers;
58
+ if (legacy && typeof legacy === 'object' && !Array.isArray(legacy)) {
59
+ data.mcpServers = { ...legacy, ...data.mcpServers };
60
+ delete data.mcp_servers;
61
+ }
62
+ } else if (!data.mcp_servers) {
63
+ data.mcp_servers = {};
64
+ }
65
+ return data;
66
+ } catch {
67
+ return isMcpJson ? { mcpServers: {} } : { mcp_servers: {} };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Write MCP server entry to AI client config.
73
+ * @param {object} opts
74
+ * @param {string} opts.client - cursor | claude | claude-code | windsurf | vscode
75
+ * @param {string} opts.serverName - Key for the server (e.g. 'github-adoptai')
76
+ * @param {string} opts.command - e.g. 'node'
77
+ * @param {string[]} opts.args - e.g. ['/path/to/index.js']
78
+ * @param {object} [opts.env] - Env vars for the server
79
+ * @returns {string} Human-readable integration name for success message
80
+ */
81
+ function writeConfig({ client, serverName, command, args, env }) {
82
+ const normalized = String(client).toLowerCase().trim();
83
+ if (!SUPPORTED_CLIENTS.includes(normalized)) {
84
+ const msg = `Invalid client "${client}". Supported: ${SUPPORTED_CLIENTS.join(', ')}`;
85
+ throw new Error(msg);
86
+ }
87
+
88
+ const { path: configPath, isMcpJson } = getConfigPath(normalized);
89
+ const dir = path.dirname(configPath);
90
+ if (!fs.existsSync(dir)) {
91
+ fs.mkdirSync(dir, { recursive: true });
92
+ }
93
+
94
+ const config = readConfig(configPath, isMcpJson);
95
+ const servers = isMcpJson ? config.mcpServers : config.mcp_servers;
96
+ const entry = { command, args, env: env || {} };
97
+ if (normalized === 'claude-code') {
98
+ entry.type = 'stdio';
99
+ }
100
+ servers[serverName] = entry;
101
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
102
+
103
+ const label = serverName.replace(/-adoptai$/, '');
104
+ return label.charAt(0).toUpperCase() + label.slice(1);
105
+ }
106
+
107
+ /**
108
+ * Remove an MCP server entry from client config.
109
+ * @param {object} opts
110
+ * @param {string} opts.client - cursor | claude | claude-code | windsurf | vscode
111
+ * @param {string} opts.serverName - Key to remove (e.g. 'github-adoptai')
112
+ */
113
+ function removeConfig({ client, serverName }) {
114
+ const normalized = String(client).toLowerCase().trim();
115
+ if (!SUPPORTED_CLIENTS.includes(normalized)) {
116
+ throw new Error(`Invalid client "${client}". Supported: ${SUPPORTED_CLIENTS.join(', ')}`);
117
+ }
118
+
119
+ const { path: configPath, isMcpJson } = getConfigPath(normalized);
120
+ if (!fs.existsSync(configPath)) return;
121
+
122
+ const config = readConfig(configPath, isMcpJson);
123
+ const servers = isMcpJson ? config.mcpServers : config.mcp_servers;
124
+ delete servers[serverName];
125
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
126
+ }
127
+
128
+ /**
129
+ * List adopt.ai MCP entries for a client (entries whose key ends with -adoptai or contains adoptai).
130
+ * @param {object} opts
131
+ * @param {string} opts.client - cursor | claude | claude-code | windsurf | vscode
132
+ * @returns {Array<{ name: string, command: string, args: string[] }>}
133
+ */
134
+ function listConfigs({ client }) {
135
+ const normalized = String(client).toLowerCase().trim();
136
+ if (!SUPPORTED_CLIENTS.includes(normalized)) {
137
+ throw new Error(`Invalid client "${client}". Supported: ${SUPPORTED_CLIENTS.join(', ')}`);
138
+ }
139
+
140
+ const { path: configPath, isMcpJson } = getConfigPath(normalized);
141
+ if (!fs.existsSync(configPath)) return [];
142
+
143
+ const config = readConfig(configPath, isMcpJson);
144
+ const servers = isMcpJson ? config.mcpServers : config.mcp_servers;
145
+ return Object.entries(servers)
146
+ .filter(([name]) => /adoptai/i.test(name))
147
+ .map(([name, entry]) => ({
148
+ name,
149
+ command: entry.command || '',
150
+ args: entry.args || [],
151
+ }));
152
+ }
153
+
154
+ module.exports = {
155
+ SUPPORTED_CLIENTS,
156
+ getConfigPath,
157
+ readConfig,
158
+ writeConfig,
159
+ removeConfig,
160
+ listConfigs,
161
+ };