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
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # adoptai-mcp
2
+
3
+ Single CLI to connect **Cursor**, **Claude Desktop**, **Windsurf**, or **VS Code** to AdoptAI MCP integrations (GitHub, Salesforce, Notion, Figma, Canva).
4
+
5
+ > Published npm name is **`adoptai-mcp`** (no org scope yet). When the `@adoptai` org is ready, rename the package to `@adoptai/mcp` and update MCP config args accordingly.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ npx adoptai-mcp add --app github --client cursor
11
+ ```
12
+
13
+ One command walks through credentials, validates them against the live API, encrypts and stores them under `~/.adoptai/`, and adds an `mcpServers` entry to your client config.
14
+
15
+ ## Commands
16
+
17
+ | Command | Description |
18
+ |--------|-------------|
19
+ | `add --app <name> --client <client>` | Interactive setup for one app |
20
+ | `add --apps a,b,c --client <client>` | Set up several apps in sequence |
21
+ | `remove --app <name> --client <client>` | Remove server from client; optional credential delete |
22
+ | `list` | Apps, tool counts, and whether credentials exist |
23
+ | `status` | Which clients reference AdoptAI servers and total tools for authenticated apps |
24
+ | `serve --app <name>` | **Internal** — stdio MCP server (used by client configs) |
25
+
26
+ Clients: `cursor` \| `claude` \| `windsurf` \| `vscode` (VS Code uses `code --add-mcp` when available).
27
+
28
+ ## MCP config shape
29
+
30
+ Each app is registered as `<app>-adoptai`, for example:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "salesforce-adoptai": {
36
+ "command": "npx",
37
+ "args": ["-y", "adoptai-mcp", "serve", "--app", "salesforce"],
38
+ "env": {}
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Build (from repo)
45
+
46
+ ```bash
47
+ cd packages/adoptai-mcp
48
+ npm install
49
+ npm run build
50
+ npm link
51
+ adoptai-mcp list
52
+ ```
53
+
54
+ `npm run build` runs TypeScript and copies bundled integration sources plus `specs/` into `dist/` so the published tarball does not depend on the monorepo `integrations/` tree.
55
+
56
+ ## Local checks
57
+
58
+ 1. `node --check dist/cli/index.js`
59
+ 2. `adoptai-mcp list` (after `npm link`)
60
+ 3. `adoptai-mcp add --app github --client cursor` (requires network + valid token)
61
+ 4. Confirm `~/.cursor/mcp.json` (or your client’s file) contains the new server
62
+ 5. Restart the client and confirm tools appear
63
+
64
+ ## Salesforce authentication
65
+
66
+ The CLI uses **SOAP login** (username + password + security token) and stores the resulting **session id** as the REST bearer token. Use **Sandbox** when prompted if your org logs in via `test.salesforce.com`.
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli/index.js';
@@ -0,0 +1 @@
1
+ export const appSlug = 'canva';
@@ -0,0 +1 @@
1
+ export const appSlug = 'figma';
@@ -0,0 +1,2 @@
1
+ /** GitHub aggregate MCP (all github-* REST surfaces). */
2
+ export const appSlug = 'github';
@@ -0,0 +1 @@
1
+ export const appSlug = 'notion';
@@ -0,0 +1,20 @@
1
+ export const APP_SLUGS = ['github', 'salesforce', 'notion', 'figma', 'canva'];
2
+ export function isAppSlug(s) {
3
+ return APP_SLUGS.includes(s);
4
+ }
5
+ export function parseAppList(arg) {
6
+ if (!arg?.trim())
7
+ return [];
8
+ return arg
9
+ .split(',')
10
+ .map((a) => a.trim().toLowerCase())
11
+ .filter((a) => isAppSlug(a));
12
+ }
13
+ /** Resolve tools module URL relative to dist/apps/registry.js or dist/server/*.js. */
14
+ export function toolsModuleUrl(app) {
15
+ return new URL(`../integrations/${app}/tools.js`, import.meta.url).href;
16
+ }
17
+ export async function countToolsForApp(app) {
18
+ const mod = await import(toolsModuleUrl(app));
19
+ return Array.isArray(mod.tools) ? mod.tools.length : 0;
20
+ }
@@ -0,0 +1 @@
1
+ export const appSlug = 'salesforce';
@@ -0,0 +1,532 @@
1
+ import axios from 'axios';
2
+ import chalk from 'chalk';
3
+ import { randomBytes, createHash } from 'node:crypto';
4
+ import { createServer } from 'node:http';
5
+ import { spawn } from 'node:child_process';
6
+ import inquirer from 'inquirer';
7
+ import ora from 'ora';
8
+ import { countToolsForApp, isAppSlug } from '../apps/registry.js';
9
+ import { getCredentialsStore, hasAppCredentials } from '../config/credentials.js';
10
+ import { isClientId, upsertAdoptaiMcpServer } from '../config/clients.js';
11
+ const NOTION_VERSION = '2025-09-03';
12
+ const CANVA_AUTH_URL = 'https://www.canva.com/api/oauth/authorize';
13
+ const CANVA_TOKEN_URL = 'https://api.canva.com/rest/v1/oauth/token';
14
+ const CANVA_DEFAULT_SCOPES = [
15
+ 'design:permission:read',
16
+ 'brandtemplate:content:write',
17
+ 'folder:permission:write',
18
+ 'app:read',
19
+ 'comment:write',
20
+ 'asset:write',
21
+ 'profile:read',
22
+ 'brandtemplate:meta:read',
23
+ 'asset:read',
24
+ 'design:content:read',
25
+ 'app:write',
26
+ 'design:content:write',
27
+ 'design:permission:write',
28
+ 'brandtemplate:content:read',
29
+ 'comment:read',
30
+ 'folder:permission:read',
31
+ 'folder:write',
32
+ 'folder:read',
33
+ ].join(' ');
34
+ function escapeXml(s) {
35
+ return s
36
+ .replace(/&/g, '&amp;')
37
+ .replace(/</g, '&lt;')
38
+ .replace(/>/g, '&gt;')
39
+ .replace(/"/g, '&quot;')
40
+ .replace(/'/g, '&apos;');
41
+ }
42
+ function openBrowser(url) {
43
+ if (process.platform === 'darwin') {
44
+ spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
45
+ }
46
+ else if (process.platform === 'win32') {
47
+ spawn('cmd', ['/c', 'start', '""', url], { detached: true, stdio: 'ignore' }).unref();
48
+ }
49
+ else {
50
+ spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
51
+ }
52
+ }
53
+ function pkcePair() {
54
+ const codeVerifier = randomBytes(32).toString('base64url');
55
+ const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
56
+ return { codeVerifier, codeChallenge };
57
+ }
58
+ async function salesforceSoapLogin(opts) {
59
+ const { username, password, securityToken, loginHost } = opts;
60
+ const pw = password + (securityToken || '');
61
+ const body = `<?xml version="1.0" encoding="utf-8" ?>
62
+ <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:partner.soap.sforce.com">
63
+ <soapenv:Body>
64
+ <urn:login>
65
+ <urn:username>${escapeXml(username)}</urn:username>
66
+ <urn:password>${escapeXml(pw)}</urn:password>
67
+ </urn:login>
68
+ </soapenv:Body>
69
+ </soapenv:Envelope>`;
70
+ const url = `${loginHost.replace(/\/$/, '')}/services/Soap/u/59.0`;
71
+ const res = await axios.post(url, body, {
72
+ headers: {
73
+ 'Content-Type': 'text/xml; charset=UTF-8',
74
+ SOAPAction: '""',
75
+ },
76
+ validateStatus: () => true,
77
+ timeout: 60000,
78
+ });
79
+ if (res.status >= 400) {
80
+ throw new Error(`SOAP login HTTP ${res.status}: ${String(res.data).slice(0, 500)}`);
81
+ }
82
+ const xml = typeof res.data === 'string' ? res.data : String(res.data);
83
+ const fault = xml.match(/<faultstring>([^<]*)<\/faultstring>/);
84
+ if (fault)
85
+ throw new Error(fault[1] || 'Salesforce login failed');
86
+ const sid = xml.match(/<sessionId>([^<]+)<\/sessionId>/);
87
+ const surl = xml.match(/<serverUrl>([^<]+)<\/serverUrl>/);
88
+ if (!sid?.[1] || !surl?.[1])
89
+ throw new Error('Could not parse Salesforce login response');
90
+ const u = new URL(surl[1]);
91
+ const instanceUrl = `${u.protocol}//${u.host}`;
92
+ return { sessionId: sid[1], instanceUrl };
93
+ }
94
+ async function testSalesforceLimits(instanceUrl, sessionId) {
95
+ const url = `${instanceUrl.replace(/\/$/, '')}/services/data/v59.0/limits`;
96
+ const res = await axios.get(url, {
97
+ headers: { Authorization: `Bearer ${sessionId}` },
98
+ validateStatus: () => true,
99
+ timeout: 30000,
100
+ });
101
+ if (res.status < 200 || res.status >= 300) {
102
+ throw new Error(`Limits check failed (${res.status})`);
103
+ }
104
+ }
105
+ async function testGithubToken(token) {
106
+ const res = await axios.get('https://api.github.com/user', {
107
+ headers: { Authorization: `Bearer ${token}` },
108
+ validateStatus: () => true,
109
+ timeout: 20000,
110
+ });
111
+ if (res.status < 200 || res.status >= 300) {
112
+ throw new Error(res.data?.message || `GitHub API returned ${res.status}`);
113
+ }
114
+ }
115
+ async function testNotionToken(token) {
116
+ const res = await axios.get('https://api.notion.com/v1/users/me', {
117
+ headers: { Authorization: `Bearer ${token}`, 'Notion-Version': NOTION_VERSION },
118
+ validateStatus: () => true,
119
+ timeout: 20000,
120
+ });
121
+ if (res.status < 200 || res.status >= 300) {
122
+ throw new Error(res.data?.message || `Notion API returned ${res.status}`);
123
+ }
124
+ }
125
+ async function testFigmaToken(token) {
126
+ const res = await axios.get('https://api.figma.com/v1/me', {
127
+ headers: { 'X-Figma-Token': token },
128
+ validateStatus: () => true,
129
+ timeout: 20000,
130
+ });
131
+ if (res.status < 200 || res.status >= 300) {
132
+ throw new Error(res.data?.err || `Figma API returned ${res.status}`);
133
+ }
134
+ }
135
+ async function testCanvaToken(accessToken) {
136
+ const res = await axios.get('https://api.canva.com/rest/v1/users/me', {
137
+ headers: { Authorization: `Bearer ${accessToken}` },
138
+ validateStatus: () => true,
139
+ timeout: 20000,
140
+ });
141
+ if (res.status < 200 || res.status >= 300) {
142
+ throw new Error(`Canva API returned ${res.status}`);
143
+ }
144
+ }
145
+ async function canvaOAuthFlow(clientId, clientSecret) {
146
+ const { codeVerifier, codeChallenge } = pkcePair();
147
+ const state = randomBytes(16).toString('hex');
148
+ const listenPort = 3000;
149
+ const redirectUri = `http://127.0.0.1:${listenPort}/callback`;
150
+ const server = await new Promise((resolve, reject) => {
151
+ const s = createServer();
152
+ s.once('error', (err) => reject(err));
153
+ s.listen(listenPort, '127.0.0.1', () => resolve(s));
154
+ }).catch((err) => {
155
+ if (err?.code === 'EADDRINUSE') {
156
+ throw new Error(`Port ${listenPort} is already in use. Stop the other process or set CANVA_REDIRECT_URI to another localhost port in a future version.`);
157
+ }
158
+ throw err;
159
+ });
160
+ const scope = process.env.CANVA_SCOPES?.trim() || CANVA_DEFAULT_SCOPES;
161
+ const authParams = new URLSearchParams({
162
+ response_type: 'code',
163
+ client_id: clientId,
164
+ redirect_uri: redirectUri,
165
+ scope,
166
+ state,
167
+ code_challenge: codeChallenge,
168
+ code_challenge_method: 's256',
169
+ });
170
+ const authorizeFull = `${CANVA_AUTH_URL}?${authParams.toString()}`;
171
+ const result = new Promise((resolve, reject) => {
172
+ server.on('request', (req, res) => {
173
+ try {
174
+ const u = new URL(req.url || '/', `http://127.0.0.1:${listenPort}`);
175
+ if (u.pathname !== '/callback') {
176
+ res.writeHead(404);
177
+ res.end();
178
+ return;
179
+ }
180
+ const code = u.searchParams.get('code');
181
+ const returned = u.searchParams.get('state');
182
+ const err = u.searchParams.get('error');
183
+ if (err) {
184
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
185
+ res.end(`Error: ${err}`);
186
+ server.close();
187
+ reject(new Error(u.searchParams.get('error_description') || err));
188
+ return;
189
+ }
190
+ if (!code || returned !== state) {
191
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
192
+ res.end('Invalid callback');
193
+ server.close();
194
+ reject(new Error('Invalid OAuth callback'));
195
+ return;
196
+ }
197
+ res.writeHead(200, { 'Content-Type': 'text/html' });
198
+ res.end('<html><body>You can close this window and return to the terminal.</body></html>');
199
+ server.close();
200
+ resolve(code);
201
+ }
202
+ catch (e) {
203
+ reject(e);
204
+ }
205
+ });
206
+ });
207
+ openBrowser(authorizeFull);
208
+ let code;
209
+ try {
210
+ code = await result;
211
+ }
212
+ catch (e) {
213
+ server.close();
214
+ throw e;
215
+ }
216
+ const body = new URLSearchParams({
217
+ grant_type: 'authorization_code',
218
+ code,
219
+ redirect_uri: redirectUri,
220
+ code_verifier: codeVerifier,
221
+ client_id: clientId,
222
+ client_secret: clientSecret,
223
+ });
224
+ const tok = await axios.post(CANVA_TOKEN_URL, body.toString(), {
225
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
226
+ timeout: 60000,
227
+ validateStatus: () => true,
228
+ });
229
+ if (tok.status < 200 || tok.status >= 300) {
230
+ const hint = typeof tok.data === 'object' ? JSON.stringify(tok.data) : String(tok.data);
231
+ throw new Error(`Canva token exchange failed (${tok.status}): ${hint}`);
232
+ }
233
+ const d = tok.data;
234
+ const accessToken = d.access_token;
235
+ const refreshToken = d.refresh_token;
236
+ const expiresIn = Number(d.expires_in) || 14_400;
237
+ if (!accessToken || !refreshToken) {
238
+ throw new Error('Token response missing access_token or refresh_token');
239
+ }
240
+ const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
241
+ return { accessToken, refreshToken, expiresAt };
242
+ }
243
+ async function flowGithub(store) {
244
+ const { token } = await inquirer.prompt([
245
+ {
246
+ type: 'password',
247
+ name: 'token',
248
+ message: 'Personal Access Token (from github.com/settings/tokens): ',
249
+ mask: '*',
250
+ },
251
+ ]);
252
+ const t = String(token || '').trim();
253
+ if (!t)
254
+ throw new Error('Token is required');
255
+ const spin = ora('Testing connection…').start();
256
+ try {
257
+ await testGithubToken(t);
258
+ spin.succeed('Connected to GitHub');
259
+ }
260
+ catch (e) {
261
+ spin.fail('Connection failed');
262
+ throw e;
263
+ }
264
+ const savedAt = new Date().toISOString();
265
+ store.set('github', { token: t, savedAt });
266
+ }
267
+ async function flowSalesforce(store) {
268
+ const answers = await inquirer.prompt([
269
+ {
270
+ type: 'input',
271
+ name: 'instanceHint',
272
+ message: 'Instance URL (e.g. https://yourorg.salesforce.com): ',
273
+ },
274
+ { type: 'input', name: 'username', message: 'Username: ' },
275
+ { type: 'password', name: 'password', message: 'Password: ', mask: '*' },
276
+ { type: 'password', name: 'securityToken', message: 'Security Token: ', mask: '*' },
277
+ {
278
+ type: 'confirm',
279
+ name: 'sandbox',
280
+ message: 'Is this a Sandbox org (login at test.salesforce.com)?',
281
+ default: false,
282
+ },
283
+ ]);
284
+ const loginHost = answers.sandbox ? 'https://test.salesforce.com' : 'https://login.salesforce.com';
285
+ const spin = ora('Testing connection (SOAP login + REST limits)…').start();
286
+ let sessionId;
287
+ let instanceUrl;
288
+ try {
289
+ ({ sessionId, instanceUrl } = await salesforceSoapLogin({
290
+ username: String(answers.username).trim(),
291
+ password: String(answers.password),
292
+ securityToken: String(answers.securityToken || ''),
293
+ loginHost,
294
+ }));
295
+ await testSalesforceLimits(instanceUrl, sessionId);
296
+ spin.succeed('Connected to Salesforce! (API v59.0)');
297
+ }
298
+ catch (e) {
299
+ spin.fail('Connection failed');
300
+ throw e;
301
+ }
302
+ const hint = String(answers.instanceHint || '')
303
+ .trim()
304
+ .replace(/\/$/, '');
305
+ if (hint && hint.startsWith('http')) {
306
+ try {
307
+ const parsed = new URL(hint);
308
+ const normalized = `${parsed.protocol}//${parsed.host}`;
309
+ if (normalized.replace(/\/$/, '') !== instanceUrl.replace(/\/$/, '')) {
310
+ console.log(chalk.yellow(`Note: Using instance URL from login response (${instanceUrl}), not the hint you entered.`));
311
+ }
312
+ }
313
+ catch {
314
+ /* ignore */
315
+ }
316
+ }
317
+ const savedAt = new Date().toISOString();
318
+ store.set('salesforce', {
319
+ instanceUrl,
320
+ username: String(answers.username).trim(),
321
+ accessToken: sessionId,
322
+ savedAt,
323
+ });
324
+ }
325
+ async function flowNotion(store) {
326
+ const { token } = await inquirer.prompt([
327
+ {
328
+ type: 'password',
329
+ name: 'token',
330
+ message: 'Integration Token (from notion.so/my-integrations): ',
331
+ mask: '*',
332
+ },
333
+ ]);
334
+ const t = String(token || '').trim();
335
+ if (!t)
336
+ throw new Error('Token is required');
337
+ const spin = ora('Testing connection…').start();
338
+ try {
339
+ await testNotionToken(t);
340
+ spin.succeed('Connected to Notion');
341
+ }
342
+ catch (e) {
343
+ spin.fail('Connection failed');
344
+ throw e;
345
+ }
346
+ store.set('notion', { token: t, savedAt: new Date().toISOString() });
347
+ }
348
+ async function flowFigma(store) {
349
+ const { token } = await inquirer.prompt([
350
+ {
351
+ type: 'password',
352
+ name: 'token',
353
+ message: 'Personal Access Token (from figma.com/settings): ',
354
+ mask: '*',
355
+ },
356
+ ]);
357
+ const t = String(token || '').trim();
358
+ if (!t)
359
+ throw new Error('Token is required');
360
+ const spin = ora('Testing connection…').start();
361
+ try {
362
+ await testFigmaToken(t);
363
+ spin.succeed('Connected to Figma');
364
+ }
365
+ catch (e) {
366
+ spin.fail('Connection failed');
367
+ throw e;
368
+ }
369
+ store.set('figma', { token: t, savedAt: new Date().toISOString() });
370
+ }
371
+ async function flowCanva(store) {
372
+ const { open, clientId, clientSecret } = await inquirer.prompt([
373
+ {
374
+ type: 'confirm',
375
+ name: 'open',
376
+ message: 'Open browser for Canva OAuth?',
377
+ default: true,
378
+ },
379
+ { type: 'input', name: 'clientId', message: 'Canva integration Client ID: ' },
380
+ { type: 'password', name: 'clientSecret', message: 'Canva integration Client secret: ', mask: '*' },
381
+ ]);
382
+ if (!open)
383
+ throw new Error('Canva setup requires completing OAuth in the browser.');
384
+ const cid = String(clientId || '').trim();
385
+ const sec = String(clientSecret || '').trim();
386
+ if (!cid || !sec)
387
+ throw new Error('Client ID and Client secret are required');
388
+ console.log(chalk.dim('\nLocal callback listens on http://127.0.0.1:3000/callback — add this URL to your Canva integration.\n'));
389
+ const spin = ora('Waiting for Canva OAuth callback on port 3000…').start();
390
+ let tokens;
391
+ try {
392
+ tokens = await canvaOAuthFlow(cid, sec);
393
+ spin.succeed('Canva OAuth complete');
394
+ }
395
+ catch (e) {
396
+ spin.fail('Canva OAuth failed');
397
+ throw e;
398
+ }
399
+ const spin2 = ora('Verifying token…').start();
400
+ try {
401
+ await testCanvaToken(tokens.accessToken);
402
+ spin2.succeed('Connected to Canva');
403
+ }
404
+ catch (e) {
405
+ spin2.fail('Verification failed');
406
+ throw e;
407
+ }
408
+ store.set('canva', {
409
+ accessToken: tokens.accessToken,
410
+ refreshToken: tokens.refreshToken,
411
+ clientId: cid,
412
+ clientSecret: sec,
413
+ expiresAt: tokens.expiresAt,
414
+ savedAt: new Date().toISOString(),
415
+ });
416
+ }
417
+ async function runOneApp(app, client, store) {
418
+ console.log(chalk.bold.cyan(`\n ${app.charAt(0).toUpperCase() + app.slice(1)} Setup`));
419
+ console.log(chalk.dim(' ────────────────────────────────────'));
420
+ if (hasAppCredentials(app)) {
421
+ const { again } = await inquirer.prompt([
422
+ {
423
+ type: 'confirm',
424
+ name: 'again',
425
+ message: `${app} is already configured. Re-authenticate?`,
426
+ default: false,
427
+ },
428
+ ]);
429
+ if (!again) {
430
+ console.log(chalk.yellow('Skipped (keeping existing credentials).'));
431
+ const spin = ora('Updating client config…').start();
432
+ try {
433
+ const written = upsertAdoptaiMcpServer(client, app);
434
+ spin.succeed(`Config updated (${written})`);
435
+ }
436
+ catch (e) {
437
+ spin.fail('Config update failed');
438
+ throw e;
439
+ }
440
+ const n = await countToolsForApp(app);
441
+ printSuccess(app, n, client);
442
+ return;
443
+ }
444
+ }
445
+ let ok = false;
446
+ let lastErr;
447
+ while (!ok) {
448
+ try {
449
+ switch (app) {
450
+ case 'github':
451
+ await flowGithub(store);
452
+ break;
453
+ case 'salesforce':
454
+ await flowSalesforce(store);
455
+ break;
456
+ case 'notion':
457
+ await flowNotion(store);
458
+ break;
459
+ case 'figma':
460
+ await flowFigma(store);
461
+ break;
462
+ case 'canva':
463
+ await flowCanva(store);
464
+ break;
465
+ default:
466
+ throw new Error(`Unknown app: ${app}`);
467
+ }
468
+ ok = true;
469
+ }
470
+ catch (err) {
471
+ lastErr = err;
472
+ const msg = err instanceof Error ? err.message : String(err);
473
+ console.log(chalk.red(`\n ${msg}`));
474
+ const { retry } = await inquirer.prompt([
475
+ {
476
+ type: 'confirm',
477
+ name: 'retry',
478
+ message: 'Try again with different credentials?',
479
+ default: true,
480
+ },
481
+ ]);
482
+ if (!retry)
483
+ throw lastErr;
484
+ }
485
+ }
486
+ const spinSave = ora('Saving credentials…').start();
487
+ spinSave.succeed('Credentials saved');
488
+ const spinCfg = ora('Updating client config…').start();
489
+ try {
490
+ const pathOrLabel = upsertAdoptaiMcpServer(client, app);
491
+ spinCfg.succeed(`Config written (${pathOrLabel})`);
492
+ }
493
+ catch (e) {
494
+ spinCfg.fail('Config update failed');
495
+ throw e;
496
+ }
497
+ const n = await countToolsForApp(app);
498
+ printSuccess(app, n, client);
499
+ }
500
+ function printSuccess(app, toolCount, client) {
501
+ const label = app.charAt(0).toUpperCase() + app.slice(1);
502
+ console.log(chalk.green('\n ──────────────────────────────────────────'));
503
+ console.log(chalk.green.bold(` 🎉 ${label} is ready!`));
504
+ console.log(chalk.green(` ${toolCount} tools available in ${client}`));
505
+ console.log(chalk.cyan('\n 🔄 Restart Cursor (or your client) to activate.'));
506
+ console.log(chalk.green(' ──────────────────────────────────────────\n'));
507
+ }
508
+ export async function runAdd(argv) {
509
+ const clientRaw = argv.client;
510
+ if (!isClientId(clientRaw)) {
511
+ console.error(chalk.red(`Unknown client "${clientRaw}". Use: cursor | claude | windsurf | vscode`));
512
+ process.exit(1);
513
+ }
514
+ const client = clientRaw;
515
+ const fromApps = argv.apps ? argv.apps.split(',').map((s) => s.trim().toLowerCase()) : [];
516
+ const single = argv.app ? [argv.app.trim().toLowerCase()] : [];
517
+ const names = fromApps.length ? fromApps : single;
518
+ if (!names.length) {
519
+ console.error(chalk.red('Provide --app <name> or --apps a,b,c'));
520
+ process.exit(1);
521
+ }
522
+ const apps = names.filter(isAppSlug);
523
+ if (apps.length !== names.length) {
524
+ const bad = names.filter((a) => !isAppSlug(a));
525
+ console.error(chalk.red(`Unknown app(s): ${bad.join(', ')}`));
526
+ process.exit(1);
527
+ }
528
+ const store = getCredentialsStore();
529
+ for (const app of apps) {
530
+ await runOneApp(app, client, store);
531
+ }
532
+ }
@@ -0,0 +1,39 @@
1
+ import yargs from 'yargs';
2
+ import { hideBin } from 'yargs/helpers';
3
+ import { runAdd } from './add.js';
4
+ import { runList } from './list.js';
5
+ import { runRemove } from './remove.js';
6
+ import { runServe } from './serve.js';
7
+ import { runStatus } from './status.js';
8
+ await yargs(hideBin(process.argv))
9
+ .scriptName('adoptai-mcp')
10
+ .usage('$0 <command> [options]')
11
+ .command('add', 'Authenticate, save credentials, and register the MCP server in your client', (y) => y
12
+ .option('app', { type: 'string', describe: 'Single app id' })
13
+ .option('apps', { type: 'string', describe: 'Comma-separated app ids' })
14
+ .option('client', { type: 'string', demandOption: true, describe: 'cursor | claude | windsurf | vscode' }), async (argv) => {
15
+ await runAdd({
16
+ app: argv.app,
17
+ apps: argv.apps,
18
+ client: argv.client,
19
+ });
20
+ })
21
+ .command('remove', 'Remove AdoptAI MCP from a client config; optionally delete stored credentials', (y) => y
22
+ .option('app', { type: 'string', demandOption: true })
23
+ .option('client', { type: 'string', demandOption: true }), async (argv) => {
24
+ await runRemove({ app: argv.app, client: argv.client });
25
+ })
26
+ .command('list', 'List apps, tool counts, and authentication status', {}, async () => {
27
+ await runList();
28
+ })
29
+ .command('status', 'Show client configs and connected apps', {}, async () => {
30
+ await runStatus();
31
+ })
32
+ .command('serve', 'Internal: run stdio MCP for one app (used by client config)', (y) => y.option('app', { type: 'string', demandOption: true }), async (argv) => {
33
+ await runServe({ app: argv.app });
34
+ })
35
+ .demandCommand(1, 'Choose a command: add, remove, list, status, serve')
36
+ .strict()
37
+ .help()
38
+ .alias('h', 'help')
39
+ .parseAsync();