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,199 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file doctor.js
4
+ * Health check CLI for adopt.ai MCP integrations.
5
+ * Usage: npx adoptai-doctor [--client cursor|claude|windsurf|vscode]
6
+ */
7
+
8
+ const path = require('path');
9
+ const { getConfigPath, listConfigs, SUPPORTED_CLIENTS } = require('./config-writer.js');
10
+ const { getCredentials, isTokenExpired, listAllCredentials } = require('./auth-manager.js');
11
+
12
+ const KNOWN_INTEGRATIONS = [
13
+ { appId: 'github', serverName: 'github-adoptai', setupCmd: 'npx adoptai-github-mcp --client' },
14
+ { appId: 'slack', serverName: 'slack-adoptai', setupCmd: 'npx adoptai-slack-mcp --client' },
15
+ { appId: '6sense', serverName: '6sense-adoptai', setupCmd: 'npx adoptai-6sense-mcp --client' },
16
+ { appId: 'zluri', serverName: 'zluri-adoptai', setupCmd: 'npx adoptai-zluri-mcp --client' },
17
+ { appId: 'hubspot', serverName: 'hubspot-adoptai', setupCmd: 'npx adoptai-hubspot-mcp --client' },
18
+ { appId: 'pipedrive', serverName: 'pipedrive-adoptai', setupCmd: 'npx adoptai-pipedrive-mcp --client' },
19
+ ];
20
+
21
+ /**
22
+ * Parse argv for --client.
23
+ * @returns {{ client: string }}
24
+ */
25
+ function parseArgs() {
26
+ const args = process.argv.slice(2);
27
+ let client = 'cursor';
28
+ for (let i = 0; i < args.length; i++) {
29
+ if ((args[i] === '--client' || args[i] === '-c') && args[i + 1]) {
30
+ client = args[i + 1].toLowerCase().trim();
31
+ break;
32
+ }
33
+ }
34
+ return { client };
35
+ }
36
+
37
+ /**
38
+ * Check a single integration: config, token, expiry. No API ping (would require integration-specific code).
39
+ * 6sense gets special handling: expiry in hours/minutes, expired = warning with fix cmd.
40
+ * @param {object} integration - { appId, serverName, setupCmd }
41
+ * @param {string} client - cursor | claude | windsurf | vscode
42
+ * @returns {{ status: 'healthy'|'warning'|'broken', message: string, fixCmd?: string }}
43
+ */
44
+ function checkIntegration(integration, client) {
45
+ const { appId, serverName, setupCmd } = integration;
46
+ const configs = listConfigs({ client });
47
+ const hasConfig = configs.some((c) => c.name === serverName);
48
+ const creds = getCredentials(appId);
49
+ const hasToken = !!(creds && creds.token);
50
+ const expired = isTokenExpired(appId);
51
+
52
+ if (!hasConfig) {
53
+ return {
54
+ status: 'broken',
55
+ message: 'not in config — add with setup',
56
+ fixCmd: `${setupCmd} ${client}`,
57
+ };
58
+ }
59
+ if (!hasToken) {
60
+ return {
61
+ status: 'broken',
62
+ message: 'token not found — run setup',
63
+ fixCmd: `${setupCmd} ${client}`,
64
+ };
65
+ }
66
+
67
+ // 6sense: token expires ~24h; show warning when expired or <2h left
68
+ if (appId === '6sense') {
69
+ if (expired) {
70
+ return {
71
+ status: 'warning',
72
+ message: 'token EXPIRED — re-run setup',
73
+ fixCmd: `${setupCmd} ${client}`,
74
+ };
75
+ }
76
+ const expiresAt = creds.expiresAt;
77
+ if (expiresAt) {
78
+ const ms = new Date(expiresAt).getTime() - Date.now();
79
+ const hoursLeft = ms / (1000 * 60 * 60);
80
+ const minutesLeft = ms / (1000 * 60);
81
+ if (hoursLeft < 2 && minutesLeft > 0) {
82
+ const mins = Math.round(minutesLeft);
83
+ return { status: 'warning', message: `token expires in ${mins} minutes — refresh soon` };
84
+ }
85
+ if (minutesLeft <= 0) {
86
+ return {
87
+ status: 'warning',
88
+ message: 'token EXPIRED — re-run setup',
89
+ fixCmd: `${setupCmd} ${client}`,
90
+ };
91
+ }
92
+ const hours = Math.round(hoursLeft);
93
+ return { status: 'healthy', message: `token valid for ${hours} hours` };
94
+ }
95
+ return { status: 'healthy', message: 'token valid' };
96
+ }
97
+
98
+ // Zluri: handle refresh token; show "authenticated as <name>"
99
+ if (appId === 'zluri') {
100
+ const hasRefresh = !!(creds.refreshToken);
101
+ if (expired) {
102
+ if (hasRefresh) {
103
+ return {
104
+ status: 'warning',
105
+ message: 'token expired but refresh available — run refresh',
106
+ fixCmd: `node integrations/zluri/setup.js --refresh`,
107
+ };
108
+ }
109
+ return {
110
+ status: 'broken',
111
+ message: 'token expired, no refresh — re-run setup',
112
+ fixCmd: `${setupCmd} ${client}`,
113
+ };
114
+ }
115
+ const name = creds.name || creds.email || 'user';
116
+ const expiresAt = creds.expiresAt;
117
+ if (expiresAt) {
118
+ const ms = new Date(expiresAt).getTime() - Date.now();
119
+ const hours = Math.round(ms / (60 * 60 * 1000));
120
+ if (hours <= 2 && hours > 0) {
121
+ return { status: 'warning', message: `authenticated as ${name}, expires in ${hours} hour(s)` };
122
+ }
123
+ if (hours <= 0) {
124
+ return {
125
+ status: 'broken',
126
+ message: 'token expired — re-run setup',
127
+ fixCmd: `${setupCmd} ${client}`,
128
+ };
129
+ }
130
+ return { status: 'healthy', message: `authenticated as ${name}, expires in ${hours} hours` };
131
+ }
132
+ return { status: 'healthy', message: `authenticated as ${name}` };
133
+ }
134
+
135
+ // Other integrations (GitHub, Slack)
136
+ if (expired) {
137
+ return {
138
+ status: 'warning',
139
+ message: 'token expired — re-run setup',
140
+ fixCmd: `${setupCmd} ${client}`,
141
+ };
142
+ }
143
+ const expiresAt = creds.expiresAt;
144
+ if (expiresAt) {
145
+ const ms = new Date(expiresAt).getTime() - Date.now();
146
+ const hours = Math.round(ms / (60 * 60 * 1000));
147
+ if (hours <= 2 && hours > 0) {
148
+ return { status: 'warning', message: `token expires in ${hours} hour(s)` };
149
+ }
150
+ if (hours <= 0) {
151
+ return {
152
+ status: 'warning',
153
+ message: 'token expired',
154
+ fixCmd: `${setupCmd} ${client}`,
155
+ };
156
+ }
157
+ }
158
+ if (appId === 'github') {
159
+ return { status: 'healthy', message: `authenticated as @${creds.username || 'user'}` };
160
+ }
161
+ if (appId === 'slack') {
162
+ return { status: 'healthy', message: `connected to ${creds.workspace || 'Slack'}` };
163
+ }
164
+ return { status: 'healthy', message: 'token valid' };
165
+ }
166
+
167
+ /**
168
+ * Run doctor for the given client and print report.
169
+ * @param {string} client - cursor | claude | windsurf | vscode
170
+ */
171
+ function run(client) {
172
+ if (!SUPPORTED_CLIENTS.includes(client)) {
173
+ console.error(`Invalid client "${client}". Supported: ${SUPPORTED_CLIENTS.join(', ')}`);
174
+ process.exit(1);
175
+ }
176
+
177
+ console.log(`adopt.ai Doctor — ${client}`);
178
+ console.log('─────────────────────────────────────────');
179
+
180
+ let healthy = 0;
181
+ const lines = [];
182
+
183
+ for (const integration of KNOWN_INTEGRATIONS) {
184
+ const result = checkIntegration(integration, client);
185
+ const icon = result.status === 'healthy' ? '✅' : result.status === 'warning' ? '⚠️' : '❌';
186
+ const statusLabel = result.status === 'healthy' ? 'healthy' : result.status === 'warning' ? 'warning' : 'broken';
187
+ if (result.status === 'healthy') healthy++;
188
+ let msg = `${integration.appId.padEnd(10)} ${icon} ${statusLabel.padEnd(8)} ${result.message}`;
189
+ if (result.fixCmd) msg += ` — run: ${result.fixCmd}`;
190
+ lines.push(msg);
191
+ }
192
+
193
+ lines.forEach((l) => console.log(l));
194
+ console.log('─────────────────────────────────────────');
195
+ console.log(`${healthy}/${KNOWN_INTEGRATIONS.length} integrations healthy`);
196
+ }
197
+
198
+ const { client } = parseArgs();
199
+ run(client);
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @file server-base.js
3
+ * Factory that creates a configured MCP server. Every integration uses this
4
+ * instead of setting up the Server manually.
5
+ */
6
+
7
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
8
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
9
+ const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
10
+
11
+ /**
12
+ * Create an MCP server with the given name, version, and tools.
13
+ * Sets up ListTools and CallTool handlers; wraps handlers in try/catch;
14
+ * connects to StdioServerTransport.
15
+ *
16
+ * @param {object} opts
17
+ * @param {string} opts.name - Server name (e.g. 'github-adoptai')
18
+ * @param {string} opts.version - Server version (e.g. '1.0.0')
19
+ * @param {Array<{name: string, description: string, inputSchema: object, handler: (params: object) => Promise<{content: Array<{type: 'text', text: string}>}>}>} opts.tools - Tool definitions
20
+ * @returns {{ server: import('@modelcontextprotocol/sdk').Server, start: () => Promise<void> }}
21
+ */
22
+ function createMCPServer({ name, version, tools }) {
23
+ const server = new Server(
24
+ { name, version },
25
+ { capabilities: { tools: {} } }
26
+ );
27
+
28
+ const toolsList = tools.map((t) => ({
29
+ name: t.name,
30
+ description: t.description || '',
31
+ inputSchema: t.inputSchema || { type: 'object', properties: {} },
32
+ }));
33
+
34
+ const handlerMap = new Map(tools.map((t) => [t.name, t.handler]));
35
+
36
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
37
+ tools: toolsList,
38
+ }));
39
+
40
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
41
+ const { name: toolName, arguments: args } = request.params;
42
+ const handler = handlerMap.get(toolName);
43
+ if (!handler) {
44
+ return {
45
+ content: [{ type: 'text', text: JSON.stringify({ isError: true, message: `Unknown tool: ${toolName}` }) }],
46
+ isError: true,
47
+ };
48
+ }
49
+ try {
50
+ const params = typeof args === 'object' && args !== null ? args : {};
51
+ const result = await handler(params);
52
+ if (result && Array.isArray(result.content)) {
53
+ return result;
54
+ }
55
+ return {
56
+ content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result ?? {}) }],
57
+ };
58
+ } catch (err) {
59
+ const message = err instanceof Error ? err.message : String(err);
60
+ console.error(`[${name}] Tool ${toolName} error:`, message);
61
+ return {
62
+ content: [{ type: 'text', text: JSON.stringify({ isError: true, message }) }],
63
+ isError: true,
64
+ };
65
+ }
66
+ });
67
+
68
+ let transport = null;
69
+
70
+ return {
71
+ server,
72
+ async start() {
73
+ transport = new StdioServerTransport();
74
+ await server.connect(transport);
75
+ },
76
+ };
77
+ }
78
+
79
+ module.exports = {
80
+ createMCPServer,
81
+ };
@@ -0,0 +1,3 @@
1
+ CANVA_REDIRECT_URI=http://127.0.0.1:3000/callback
2
+ CANVA_CLIENT_ID=OC-AZ0q7TlCXhBe
3
+ CANVA_CLIENT_SECRET=cnvcaXU4a8SBhzB1df_yC13mEVJfZrMLpDfmhl8S0NwZ5cd45f891314
@@ -0,0 +1,287 @@
1
+ import './env.js';
2
+ import { randomBytes, createHash } from 'node:crypto';
3
+ import { createServer } from 'node:http';
4
+ import { spawn } from 'node:child_process';
5
+ import axios from 'axios';
6
+ import { saveCredentials, getCredentials, isTokenExpired } from '../../core/auth-manager.js';
7
+
8
+ const APP_ID = 'canva';
9
+
10
+ const AUTH_URL = 'https://www.canva.com/api/oauth/authorize';
11
+ const TOKEN_URL = 'https://api.canva.com/rest/v1/oauth/token';
12
+
13
+ /** Default scopes aligned with Canva Connect dashboard / authorize URL. Override with CANVA_SCOPES (space-separated). */
14
+ const 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
+
35
+ function scopeString() {
36
+ return process.env.CANVA_SCOPES?.trim() || DEFAULT_SCOPES;
37
+ }
38
+
39
+ function requireEnv(name) {
40
+ const v = process.env[name]?.trim();
41
+ if (!v) {
42
+ throw new Error(
43
+ `${name} is not set. Add it to your environment (e.g. export ${name}=...) before running setup.`
44
+ );
45
+ }
46
+ return v;
47
+ }
48
+
49
+ function pkcePair() {
50
+ const codeVerifier = randomBytes(32).toString('base64url');
51
+ const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
52
+ return { codeVerifier, codeChallenge };
53
+ }
54
+
55
+ function openBrowser(url) {
56
+ if (process.platform === 'darwin') {
57
+ spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
58
+ } else if (process.platform === 'win32') {
59
+ spawn('cmd', ['/c', 'start', '""', url], { detached: true, stdio: 'ignore' }).unref();
60
+ } else {
61
+ spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
62
+ }
63
+ }
64
+
65
+ async function exchangeCodeForTokens({ code, codeVerifier, redirectUri, clientId, clientSecret }) {
66
+ const body = new URLSearchParams({
67
+ grant_type: 'authorization_code',
68
+ code,
69
+ redirect_uri: redirectUri,
70
+ code_verifier: codeVerifier,
71
+ client_id: clientId,
72
+ client_secret: clientSecret,
73
+ });
74
+
75
+ const res = await axios.post(TOKEN_URL, body.toString(), {
76
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
77
+ timeout: 60000,
78
+ validateStatus: () => true,
79
+ });
80
+
81
+ if (res.status < 200 || res.status >= 300) {
82
+ const hint = typeof res.data === 'object' ? JSON.stringify(res.data) : String(res.data);
83
+ throw new Error(`Canva token exchange failed (${res.status}): ${hint}`);
84
+ }
85
+
86
+ const d = res.data;
87
+ const accessToken = d.access_token;
88
+ const refreshToken = d.refresh_token;
89
+ const expiresIn = Number(d.expires_in) || 14_400;
90
+ if (!accessToken || !refreshToken) {
91
+ throw new Error('Token response missing access_token or refresh_token');
92
+ }
93
+
94
+ const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
95
+ return { accessToken, refreshToken, expiresAt, scopes: d.scope ? String(d.scope).split(/\s+/) : [] };
96
+ }
97
+
98
+ async function refreshTokens(refreshToken, clientId, clientSecret) {
99
+ const body = new URLSearchParams({
100
+ grant_type: 'refresh_token',
101
+ refresh_token: refreshToken,
102
+ client_id: clientId,
103
+ client_secret: clientSecret,
104
+ });
105
+
106
+ const res = await axios.post(TOKEN_URL, body.toString(), {
107
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
108
+ timeout: 60000,
109
+ validateStatus: () => true,
110
+ });
111
+
112
+ if (res.status < 200 || res.status >= 300) {
113
+ const hint = typeof res.data === 'object' ? JSON.stringify(res.data) : String(res.data);
114
+ throw new Error(`Canva token refresh failed (${res.status}): ${hint}`);
115
+ }
116
+
117
+ const d = res.data;
118
+ const accessToken = d.access_token;
119
+ const newRefresh = d.refresh_token || refreshToken;
120
+ const expiresIn = Number(d.expires_in) || 14_400;
121
+ if (!accessToken) throw new Error('Refresh response missing access_token');
122
+ const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
123
+ return { accessToken, refreshToken: newRefresh, expiresAt };
124
+ }
125
+
126
+ export async function runAuth() {
127
+ const clientId = requireEnv('CANVA_CLIENT_ID');
128
+ const clientSecret = requireEnv('CANVA_CLIENT_SECRET');
129
+
130
+ const { codeVerifier, codeChallenge } = pkcePair();
131
+ const state = randomBytes(16).toString('hex');
132
+
133
+ const fixedRedirect = process.env.CANVA_REDIRECT_URI?.trim();
134
+ let redirectUri;
135
+ let callbackPathname = '/callback';
136
+ let listenPort;
137
+
138
+ if (fixedRedirect) {
139
+ const ru = new URL(fixedRedirect);
140
+ if (!['127.0.0.1', 'localhost'].includes(ru.hostname)) {
141
+ throw new Error('CANVA_REDIRECT_URI must use host 127.0.0.1 or localhost');
142
+ }
143
+ listenPort = Number(ru.port);
144
+ if (!Number.isFinite(listenPort) || listenPort <= 0) {
145
+ throw new Error('CANVA_REDIRECT_URI must include an explicit port (e.g. http://127.0.0.1:3000/callback)');
146
+ }
147
+ redirectUri = fixedRedirect;
148
+ callbackPathname = ru.pathname || '/callback';
149
+ }
150
+
151
+ const server = await new Promise((resolve, reject) => {
152
+ const s = createServer();
153
+ s.once('error', reject);
154
+ s.listen(fixedRedirect ? listenPort : 0, '127.0.0.1', () => resolve(s));
155
+ });
156
+
157
+ if (!fixedRedirect) {
158
+ listenPort = server.address().port;
159
+ redirectUri = `http://127.0.0.1:${listenPort}/callback`;
160
+ }
161
+
162
+ const authParams = new URLSearchParams({
163
+ response_type: 'code',
164
+ client_id: clientId,
165
+ redirect_uri: redirectUri,
166
+ scope: scopeString(),
167
+ state,
168
+ code_challenge: codeChallenge,
169
+ // Canva documents lowercase s256 in authorize URLs (RFC 7636 allows case-insensitive; use their form).
170
+ code_challenge_method: 's256',
171
+ });
172
+
173
+ const authorizeFull = `${AUTH_URL}?${authParams.toString()}`;
174
+
175
+ console.log(`
176
+ ┌─────────────────────────────────────────────┐
177
+ │ Canva Authentication (OAuth2 + PKCE) │
178
+ │ │
179
+ │ Ensure this redirect URI is allowed in │
180
+ │ your Canva integration: │
181
+ │ ${redirectUri}
182
+ │ │
183
+ │ Opening browser to authorize… │
184
+ └─────────────────────────────────────────────┘
185
+ `);
186
+
187
+ const result = new Promise((resolve, reject) => {
188
+ server.on('request', (req, res) => {
189
+ try {
190
+ const u = new URL(req.url || '/', `http://127.0.0.1:${listenPort}`);
191
+ if (u.pathname !== callbackPathname) {
192
+ res.writeHead(404);
193
+ res.end();
194
+ return;
195
+ }
196
+ const code = u.searchParams.get('code');
197
+ const returned = u.searchParams.get('state');
198
+ const err = u.searchParams.get('error');
199
+ if (err) {
200
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
201
+ res.end(`Error: ${err}`);
202
+ server.close();
203
+ reject(new Error(u.searchParams.get('error_description') || err));
204
+ return;
205
+ }
206
+ if (!code || returned !== state) {
207
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
208
+ res.end('Invalid callback');
209
+ server.close();
210
+ reject(new Error('Invalid OAuth callback'));
211
+ return;
212
+ }
213
+ res.writeHead(200, { 'Content-Type': 'text/html' });
214
+ res.end('<html><body>You can close this window and return to the terminal.</body></html>');
215
+ server.close();
216
+ resolve(code);
217
+ } catch (e) {
218
+ reject(e);
219
+ }
220
+ });
221
+ });
222
+
223
+ openBrowser(authorizeFull);
224
+
225
+ let code;
226
+ try {
227
+ code = await result;
228
+ } catch (e) {
229
+ server.close();
230
+ throw e;
231
+ }
232
+
233
+ const tokens = await exchangeCodeForTokens({
234
+ code,
235
+ codeVerifier,
236
+ redirectUri,
237
+ clientId,
238
+ clientSecret,
239
+ });
240
+
241
+ saveCredentials(APP_ID, {
242
+ token: tokens.accessToken,
243
+ refreshToken: tokens.refreshToken,
244
+ tokenType: 'bearer',
245
+ expiresAt: tokens.expiresAt,
246
+ scopes: tokens.scopes,
247
+ savedAt: new Date().toISOString(),
248
+ });
249
+
250
+ console.log('✅ Canva tokens saved (access + refresh).');
251
+ }
252
+
253
+ export function getToken() {
254
+ return getCredentials(APP_ID)?.token || null;
255
+ }
256
+
257
+ export function buildAuthHeaders() {
258
+ const token = getToken();
259
+ if (!token) {
260
+ throw new Error('Not authenticated. Run: npx adoptai-canva-mcp --client cursor');
261
+ }
262
+ return { Authorization: `Bearer ${token}` };
263
+ }
264
+
265
+ export async function ensureAccessToken() {
266
+ const creds = getCredentials(APP_ID);
267
+ if (!creds?.token) {
268
+ throw new Error('Not authenticated. Run: npx adoptai-canva-mcp --client cursor');
269
+ }
270
+ if (!isTokenExpired(APP_ID)) return;
271
+
272
+ const clientId = requireEnv('CANVA_CLIENT_ID');
273
+ const clientSecret = requireEnv('CANVA_CLIENT_SECRET');
274
+ if (!creds.refreshToken) {
275
+ throw new Error('Access token expired and no refresh token. Run setup again.');
276
+ }
277
+
278
+ const next = await refreshTokens(creds.refreshToken, clientId, clientSecret);
279
+ saveCredentials(APP_ID, {
280
+ ...creds,
281
+ token: next.accessToken,
282
+ refreshToken: next.refreshToken,
283
+ tokenType: 'bearer',
284
+ expiresAt: next.expiresAt,
285
+ savedAt: new Date().toISOString(),
286
+ });
287
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Load integrations/canva/.env before auth reads CANVA_* variables.
3
+ */
4
+ import dotenv from 'dotenv';
5
+ import { dirname, join } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ dotenv.config({ path: join(__dirname, '.env') });
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import './env.js';
3
+ import { createMCPServer } from '../../core/server-base.js';
4
+ import { tools } from './tools.js';
5
+
6
+ const { start } = createMCPServer({
7
+ name: 'canva-adoptai',
8
+ version: '1.0.0',
9
+ tools,
10
+ });
11
+
12
+ start();
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "adoptai-canva-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Canva Connect API — designs, assets, folders, brand templates, exports, comments, users.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "adoptai-canva-mcp": "./setup.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "setup": "node setup.js",
13
+ "generate:tools": "node ../../scripts/generate-canva-tools.mjs",
14
+ "publish:adoptai": "node publish-to-adoptai.js"
15
+ },
16
+ "keywords": ["mcp", "canva", "ai", "cursor", "claude", "adopt.ai"],
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.0.0",
20
+ "axios": "^1.6.0",
21
+ "dotenv": "^16.6.1"
22
+ },
23
+ "adoptai": {
24
+ "appId": "canva",
25
+ "toolCount": 55,
26
+ "sourceFormat": "openapi_3",
27
+ "authType": "oauth2",
28
+ "specSources": ["specs/canva-api.json"],
29
+ "generatedAt": "2026-03-26T15:51:24Z"
30
+ }
31
+ }