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,568 @@
1
+ import { readFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { notionRequest } from './api.js';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ const manifest = JSON.parse(readFileSync(join(__dirname, 'endpoints.json'), 'utf8'));
9
+ const BASE_URL = manifest.baseUrl;
10
+
11
+ function text(payload) {
12
+ return {
13
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
14
+ };
15
+ }
16
+
17
+ function enrichSchema(schema, method) {
18
+ const s = structuredClone(schema);
19
+ s.properties = s.properties || {};
20
+ if (['POST', 'PUT', 'PATCH'].includes(method)) {
21
+ s.properties._rawBody = {
22
+ type: 'object',
23
+ description:
24
+ 'Optional full JSON body for nested structures (parent, properties, rich_text, etc.). When set, replaces other body fields.',
25
+ };
26
+ }
27
+ return s;
28
+ }
29
+
30
+ const baseTools = manifest.endpoints.map((endpoint) => ({
31
+ name: endpoint.name,
32
+ description: endpoint.description,
33
+ inputSchema: enrichSchema(endpoint.inputSchema, endpoint.method),
34
+ handler: async (params) => text(await notionRequest(endpoint, BASE_URL, params)),
35
+ }));
36
+
37
+ function call(method, path, params = {}) {
38
+ return notionRequest({ method, path }, BASE_URL, params);
39
+ }
40
+
41
+ function richText(content) {
42
+ return [{ type: 'text', text: { content: String(content ?? '') } }];
43
+ }
44
+
45
+ function blockText(block) {
46
+ const t = block?.type;
47
+ const value = block?.[t];
48
+ if (!value) return '';
49
+ if (Array.isArray(value.rich_text)) {
50
+ return value.rich_text.map((x) => x?.plain_text || x?.text?.content || '').join('');
51
+ }
52
+ if (Array.isArray(value.title)) {
53
+ return value.title.map((x) => x?.plain_text || x?.text?.content || '').join('');
54
+ }
55
+ return '';
56
+ }
57
+
58
+ function pageTitle(page) {
59
+ const props = page?.properties || {};
60
+ for (const p of Object.values(props)) {
61
+ if (p?.type === 'title' && Array.isArray(p.title)) {
62
+ const t = p.title.map((x) => x?.plain_text || x?.text?.content || '').join('');
63
+ if (t) return t;
64
+ }
65
+ }
66
+ return '';
67
+ }
68
+
69
+ async function listAllChildren(block_id, max_depth = 5, depth = 1) {
70
+ let cursor;
71
+ const items = [];
72
+ do {
73
+ const page = await call('GET', '/v1/blocks/{block_id}/children', {
74
+ block_id,
75
+ start_cursor: cursor,
76
+ page_size: 100,
77
+ });
78
+ const results = page?.results || [];
79
+ for (const b of results) {
80
+ const out = { ...b };
81
+ if (b.has_children && depth < max_depth) {
82
+ out.children = await listAllChildren(b.id, max_depth, depth + 1);
83
+ }
84
+ items.push(out);
85
+ }
86
+ cursor = page?.has_more ? page?.next_cursor : null;
87
+ } while (cursor);
88
+ return items;
89
+ }
90
+
91
+ function markdownToBlocks(content, defaultType = 'paragraph') {
92
+ const lines = String(content || '')
93
+ .split('\n')
94
+ .map((x) => x.replace(/\r$/, ''));
95
+ const blocks = [];
96
+ for (const line of lines) {
97
+ if (!line.trim()) continue;
98
+ let type = defaultType;
99
+ let textValue = line;
100
+ let checked = false;
101
+ if (line.startsWith('### ')) {
102
+ type = 'heading_3';
103
+ textValue = line.slice(4);
104
+ } else if (line.startsWith('## ')) {
105
+ type = 'heading_2';
106
+ textValue = line.slice(3);
107
+ } else if (line.startsWith('# ')) {
108
+ type = 'heading_1';
109
+ textValue = line.slice(2);
110
+ } else if (line.startsWith('- [ ] ')) {
111
+ type = 'to_do';
112
+ textValue = line.slice(6);
113
+ checked = false;
114
+ } else if (line.startsWith('- [x] ') || line.startsWith('- [X] ')) {
115
+ type = 'to_do';
116
+ textValue = line.slice(6);
117
+ checked = true;
118
+ } else if (/^\d+\.\s+/.test(line)) {
119
+ type = 'numbered_list_item';
120
+ textValue = line.replace(/^\d+\.\s+/, '');
121
+ } else if (line.startsWith('- ')) {
122
+ type = 'bulleted_list_item';
123
+ textValue = line.slice(2);
124
+ } else if (line.startsWith('> ')) {
125
+ type = 'quote';
126
+ textValue = line.slice(2);
127
+ } else if (line.startsWith('!! ')) {
128
+ type = 'callout';
129
+ textValue = line.slice(3);
130
+ }
131
+ const block = { object: 'block', type, [type]: { rich_text: richText(textValue) } };
132
+ if (type === 'to_do') block[type].checked = checked;
133
+ if (type === 'callout') block[type].icon = { emoji: '💡' };
134
+ blocks.push(block);
135
+ }
136
+ return blocks;
137
+ }
138
+
139
+ function buildDatabaseFilter(filter_property, filter_type, filter_value) {
140
+ const property = String(filter_property || '').trim();
141
+ const kind = String(filter_type || 'text').trim().toLowerCase();
142
+ if (!property || filter_value === undefined || filter_value === null || filter_value === '') return null;
143
+ if (kind === 'number') return { property, number: { equals: Number(filter_value) } };
144
+ if (kind === 'checkbox') return { property, checkbox: { equals: String(filter_value) === 'true' } };
145
+ if (kind === 'select') return { property, select: { equals: String(filter_value) } };
146
+ if (kind === 'date') return { property, date: { equals: String(filter_value) } };
147
+ if (kind === 'relation') return { property, relation: { contains: String(filter_value) } };
148
+ return { property, rich_text: { contains: String(filter_value) } };
149
+ }
150
+
151
+ function propertyToCell(prop) {
152
+ if (!prop || !prop.type) return '';
153
+ if (prop.type === 'title') return (prop.title || []).map((x) => x?.plain_text || '').join('');
154
+ if (prop.type === 'rich_text') return (prop.rich_text || []).map((x) => x?.plain_text || '').join('');
155
+ if (prop.type === 'number') return prop.number;
156
+ if (prop.type === 'checkbox') return prop.checkbox;
157
+ if (prop.type === 'select') return prop.select?.name || '';
158
+ if (prop.type === 'multi_select') return (prop.multi_select || []).map((x) => x.name).join(', ');
159
+ if (prop.type === 'date') return prop.date?.start || '';
160
+ if (prop.type === 'status') return prop.status?.name || '';
161
+ if (prop.type === 'url') return prop.url || '';
162
+ if (prop.type === 'email') return prop.email || '';
163
+ if (prop.type === 'phone_number') return prop.phone_number || '';
164
+ if (prop.type === 'relation') return (prop.relation || []).map((x) => x.id).join(', ');
165
+ return '';
166
+ }
167
+
168
+ const smartTools = [
169
+ {
170
+ name: 'get_page_full_content',
171
+ description:
172
+ 'Retrieves a Notion page AND all its nested block content in one call. Automatically follows has_children blocks to fetch deeply nested content. Use this instead of get_page when you need the full text content of a page, not just its properties.',
173
+ inputSchema: {
174
+ type: 'object',
175
+ properties: {
176
+ page_id: { type: 'string', description: 'Notion page ID.' },
177
+ max_depth: { type: 'number', description: 'Maximum recursion depth. Defaults to 3.' },
178
+ },
179
+ required: ['page_id'],
180
+ },
181
+ handler: async ({ page_id, max_depth = 3 }) => {
182
+ const page = await call('GET', '/v1/pages/{page_id}', { page_id });
183
+ const blocks = await listAllChildren(page_id, max_depth);
184
+ return text({ page, content_blocks: blocks });
185
+ },
186
+ },
187
+ {
188
+ name: 'create_page_with_content',
189
+ description:
190
+ 'Creates a new Notion page inside a parent page or database AND adds initial content blocks in one call. Use this when you want to create a page that already has content, instead of creating empty page then appending separately.',
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ parent_page_id: { type: 'string', description: 'Parent page ID.' },
195
+ parent_database_id: { type: 'string', description: 'Parent database ID.' },
196
+ title: { type: 'string', description: 'Page title.' },
197
+ content: { type: 'string', description: 'Markdown-like content text.' },
198
+ },
199
+ required: ['title', 'content'],
200
+ },
201
+ handler: async ({ parent_page_id, parent_database_id, title, content }) => {
202
+ if (!parent_page_id && !parent_database_id) {
203
+ throw new Error('Provide parent_page_id or parent_database_id.');
204
+ }
205
+ const children = markdownToBlocks(content, 'paragraph');
206
+ const parent = parent_database_id ? { database_id: parent_database_id } : { page_id: parent_page_id };
207
+ const properties = parent_database_id
208
+ ? { Name: { title: richText(title) } }
209
+ : { title: { title: richText(title) } };
210
+ const created = await call('POST', '/v1/pages', { _rawBody: { parent, properties, children } });
211
+ return text(created);
212
+ },
213
+ },
214
+ {
215
+ name: 'duplicate_page',
216
+ description:
217
+ 'Duplicates a Notion page including all its properties and top-level content blocks under a specified parent. Use when you need a copy of an existing page as a template.',
218
+ inputSchema: {
219
+ type: 'object',
220
+ properties: {
221
+ page_id: { type: 'string', description: 'Source page ID.' },
222
+ parent_id: { type: 'string', description: 'Destination parent ID.' },
223
+ },
224
+ required: ['page_id', 'parent_id'],
225
+ },
226
+ handler: async ({ page_id, parent_id }) => {
227
+ const sourcePage = await call('GET', '/v1/pages/{page_id}', { page_id });
228
+ const childrenResp = await call('GET', '/v1/blocks/{block_id}/children', { block_id: page_id, page_size: 100 });
229
+ const children = childrenResp?.results || [];
230
+ const payload = {
231
+ parent: { page_id: parent_id },
232
+ properties: sourcePage.properties || {},
233
+ children,
234
+ };
235
+ const duplicated = await call('POST', '/v1/pages', { _rawBody: payload });
236
+ return text({ source_page_id: page_id, duplicated_page: duplicated });
237
+ },
238
+ },
239
+ {
240
+ name: 'query_database_filtered',
241
+ description:
242
+ 'Queries a Notion database with property filters. Supports filtering by text, number, checkbox, select, date, and relation properties. Use this for targeted queries. For unfiltered full lists use query_database_all.',
243
+ inputSchema: {
244
+ type: 'object',
245
+ properties: {
246
+ database_id: { type: 'string', description: 'Database ID.' },
247
+ filter_property: { type: 'string', description: 'Property name to filter on.' },
248
+ filter_type: { type: 'string', description: 'text|number|checkbox|select|date|relation' },
249
+ filter_value: { type: 'string', description: 'Filter value.' },
250
+ page_size: { type: 'number', description: 'Page size for query.' },
251
+ },
252
+ required: ['database_id'],
253
+ },
254
+ handler: async ({ database_id, filter_property, filter_type, filter_value, page_size = 20 }) => {
255
+ const filter = buildDatabaseFilter(filter_property, filter_type, filter_value);
256
+ const data = await call('POST', '/v1/databases/{id}/query', {
257
+ id: database_id,
258
+ _rawBody: { page_size, ...(filter ? { filter } : {}) },
259
+ });
260
+ return text(data);
261
+ },
262
+ },
263
+ {
264
+ name: 'query_database_all',
265
+ description:
266
+ 'Retrieves ALL rows from a Notion database by automatically handling pagination. Use when you need the complete dataset. Warning: may be slow for large databases. For targeted queries use query_database_filtered.',
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: {
270
+ database_id: { type: 'string', description: 'Database ID.' },
271
+ max_pages: { type: 'number', description: 'Safety cap for pagination pages. Defaults to 10.' },
272
+ },
273
+ required: ['database_id'],
274
+ },
275
+ handler: async ({ database_id, max_pages = 10 }) => {
276
+ const all = [];
277
+ let start_cursor;
278
+ let page = 0;
279
+ do {
280
+ const resp = await call('POST', '/v1/databases/{id}/query', {
281
+ id: database_id,
282
+ _rawBody: { start_cursor, page_size: 100 },
283
+ });
284
+ all.push(...(resp?.results || []));
285
+ start_cursor = resp?.has_more ? resp?.next_cursor : null;
286
+ page += 1;
287
+ } while (start_cursor && page < max_pages);
288
+ return text({ database_id, total_rows: all.length, results: all, truncated: !!start_cursor });
289
+ },
290
+ },
291
+ {
292
+ name: 'query_database_sorted',
293
+ description:
294
+ 'Queries a Notion database sorted by a specific property in ascending or descending order. Use to get rows ordered by date, name, priority or any database property.',
295
+ inputSchema: {
296
+ type: 'object',
297
+ properties: {
298
+ database_id: { type: 'string', description: 'Database ID.' },
299
+ sort_property: { type: 'string', description: 'Property used for sorting.' },
300
+ sort_direction: { type: 'string', description: 'ascending or descending' },
301
+ page_size: { type: 'number', description: 'Page size for query.' },
302
+ },
303
+ required: ['database_id', 'sort_property'],
304
+ },
305
+ handler: async ({ database_id, sort_property, sort_direction = 'ascending', page_size = 20 }) => {
306
+ const resp = await call('POST', '/v1/databases/{id}/query', {
307
+ id: database_id,
308
+ _rawBody: { page_size, sorts: [{ property: sort_property, direction: sort_direction }] },
309
+ });
310
+ return text(resp);
311
+ },
312
+ },
313
+ {
314
+ name: 'get_database_rows_as_table',
315
+ description:
316
+ 'Queries a database and returns results as a clean structured table with column names and values extracted. Much easier to read than raw API response. Use when presenting database contents to a user.',
317
+ inputSchema: {
318
+ type: 'object',
319
+ properties: {
320
+ database_id: { type: 'string', description: 'Database ID.' },
321
+ page_size: { type: 'number', description: 'Number of rows to retrieve. Defaults to 20.' },
322
+ },
323
+ required: ['database_id'],
324
+ },
325
+ handler: async ({ database_id, page_size = 20 }) => {
326
+ const resp = await call('POST', '/v1/databases/{id}/query', {
327
+ id: database_id,
328
+ _rawBody: { page_size },
329
+ });
330
+ const rows = resp?.results || [];
331
+ const columns = [...new Set(rows.flatMap((r) => Object.keys(r.properties || {})))];
332
+ const table = rows.map((row) =>
333
+ Object.fromEntries(columns.map((c) => [c, propertyToCell(row.properties?.[c])]))
334
+ );
335
+ return text({ database_id, columns, rows: table });
336
+ },
337
+ },
338
+ {
339
+ name: 'append_text_content',
340
+ description:
341
+ 'Appends simple text content to a Notion page or block. Accepts plain text or markdown and converts to Notion paragraph blocks automatically. Use for simple text additions. For complex blocks with nested structure use append_complex_blocks.',
342
+ inputSchema: {
343
+ type: 'object',
344
+ properties: {
345
+ block_id: { type: 'string', description: 'Parent block/page ID.' },
346
+ content: { type: 'string', description: 'Plain text or markdown content.' },
347
+ block_type: {
348
+ type: 'string',
349
+ description:
350
+ 'paragraph|heading_1|heading_2|heading_3|bulleted_list_item|numbered_list_item|to_do|quote|callout',
351
+ },
352
+ },
353
+ required: ['block_id', 'content'],
354
+ },
355
+ handler: async ({ block_id, content, block_type = 'paragraph' }) => {
356
+ const children = markdownToBlocks(content, block_type);
357
+ const resp = await call('PATCH', '/v1/blocks/{block_id}/children', { block_id, _rawBody: { children } });
358
+ return text(resp);
359
+ },
360
+ },
361
+ {
362
+ name: 'append_code_block',
363
+ description:
364
+ 'Appends a code block with syntax highlighting to a Notion page. Use specifically for adding code snippets. Supports all major programming languages. For regular text use append_text_content.',
365
+ inputSchema: {
366
+ type: 'object',
367
+ properties: {
368
+ block_id: { type: 'string', description: 'Parent block/page ID.' },
369
+ code: { type: 'string', description: 'Code content.' },
370
+ language: { type: 'string', description: 'Code language. Defaults to plain text.' },
371
+ caption: { type: 'string', description: 'Optional caption.' },
372
+ },
373
+ required: ['block_id', 'code'],
374
+ },
375
+ handler: async ({ block_id, code, language = 'plain text', caption }) => {
376
+ const child = {
377
+ object: 'block',
378
+ type: 'code',
379
+ code: {
380
+ rich_text: richText(code),
381
+ language,
382
+ ...(caption ? { caption: richText(caption) } : {}),
383
+ },
384
+ };
385
+ const resp = await call('PATCH', '/v1/blocks/{block_id}/children', {
386
+ block_id,
387
+ _rawBody: { children: [child] },
388
+ });
389
+ return text(resp);
390
+ },
391
+ },
392
+ {
393
+ name: 'append_table_block',
394
+ description:
395
+ 'Appends a table to a Notion page from a 2D array of values. Automatically creates table_row children. Use when adding structured tabular data to a page.',
396
+ inputSchema: {
397
+ type: 'object',
398
+ properties: {
399
+ block_id: { type: 'string', description: 'Parent block/page ID.' },
400
+ headers: { type: 'array', description: 'Array of column headers.' },
401
+ rows: { type: 'array', description: '2D array of row values.' },
402
+ },
403
+ required: ['block_id', 'headers', 'rows'],
404
+ },
405
+ handler: async ({ block_id, headers, rows }) => {
406
+ const head = Array.isArray(headers) ? headers : [];
407
+ const bodyRows = Array.isArray(rows) ? rows : [];
408
+ const toCell = (v) => richText(v == null ? '' : String(v));
409
+ const tableChildren = [
410
+ { object: 'block', type: 'table_row', table_row: { cells: head.map((h) => toCell(h)) } },
411
+ ...bodyRows.map((r) => ({
412
+ object: 'block',
413
+ type: 'table_row',
414
+ table_row: { cells: (Array.isArray(r) ? r : []).map((v) => toCell(v)) },
415
+ })),
416
+ ];
417
+ const table = {
418
+ object: 'block',
419
+ type: 'table',
420
+ table: {
421
+ table_width: head.length,
422
+ has_column_header: true,
423
+ has_row_header: false,
424
+ children: tableChildren,
425
+ },
426
+ };
427
+ const resp = await call('PATCH', '/v1/blocks/{block_id}/children', {
428
+ block_id,
429
+ _rawBody: { children: [table] },
430
+ });
431
+ return text(resp);
432
+ },
433
+ },
434
+ {
435
+ name: 'get_all_block_children_recursive',
436
+ description:
437
+ 'Retrieves all nested block children of a block or page recursively. Follows has_children flags automatically. Returns complete block tree. Use when you need the full content structure of any block.',
438
+ inputSchema: {
439
+ type: 'object',
440
+ properties: {
441
+ block_id: { type: 'string', description: 'Block or page ID.' },
442
+ max_depth: { type: 'number', description: 'Maximum recursion depth. Defaults to 5.' },
443
+ },
444
+ required: ['block_id'],
445
+ },
446
+ handler: async ({ block_id, max_depth = 5 }) => text({ block_id, children: await listAllChildren(block_id, max_depth) }),
447
+ },
448
+ {
449
+ name: 'find_page_by_title',
450
+ description:
451
+ 'Searches Notion workspace for pages matching a title query. Returns page IDs, titles and parent info. Use this first to find a page before reading or updating it. For databases use find_database_by_title.',
452
+ inputSchema: {
453
+ type: 'object',
454
+ properties: {
455
+ title_query: { type: 'string', description: 'Title query text.' },
456
+ page_size: { type: 'number', description: 'Page size. Defaults to 10.' },
457
+ },
458
+ required: ['title_query'],
459
+ },
460
+ handler: async ({ title_query, page_size = 10 }) => {
461
+ const resp = await call('POST', '/v1/search', {
462
+ _rawBody: { query: title_query, page_size, filter: { property: 'object', value: 'page' } },
463
+ });
464
+ const out = (resp?.results || []).map((x) => ({ id: x.id, title: pageTitle(x), parent: x.parent }));
465
+ return text({ results: out, has_more: resp?.has_more, next_cursor: resp?.next_cursor });
466
+ },
467
+ },
468
+ {
469
+ name: 'find_database_by_title',
470
+ description:
471
+ 'Searches Notion workspace for databases matching a name query. Returns database IDs and schema info. Use this to find a database ID before querying it. For pages use find_page_by_title.',
472
+ inputSchema: {
473
+ type: 'object',
474
+ properties: {
475
+ title_query: { type: 'string', description: 'Database name query.' },
476
+ page_size: { type: 'number', description: 'Page size. Defaults to 10.' },
477
+ },
478
+ required: ['title_query'],
479
+ },
480
+ handler: async ({ title_query, page_size = 10 }) => {
481
+ const resp = await call('POST', '/v1/search', {
482
+ _rawBody: { query: title_query, page_size, filter: { property: 'object', value: 'database' } },
483
+ });
484
+ const out = (resp?.results || []).map((x) => ({ id: x.id, title: (x.title || []).map((t) => t.plain_text).join(''), properties: x.properties }));
485
+ return text({ results: out, has_more: resp?.has_more, next_cursor: resp?.next_cursor });
486
+ },
487
+ },
488
+ {
489
+ name: 'list_all_workspace_pages',
490
+ description:
491
+ "Lists all pages accessible to this integration in the Notion workspace. Returns page IDs, titles and parent hierarchy. Use as a discovery tool when you don't know which pages exist.",
492
+ inputSchema: {
493
+ type: 'object',
494
+ properties: {
495
+ page_size: { type: 'number', description: 'Page size. Defaults to 50.' },
496
+ start_cursor: { type: 'string', description: 'Pagination cursor.' },
497
+ },
498
+ required: [],
499
+ },
500
+ handler: async ({ page_size = 50, start_cursor }) => {
501
+ const resp = await call('POST', '/v1/search', {
502
+ _rawBody: { page_size, start_cursor, filter: { property: 'object', value: 'page' } },
503
+ });
504
+ const out = (resp?.results || []).map((x) => ({ id: x.id, title: pageTitle(x), parent: x.parent }));
505
+ return text({ results: out, has_more: resp?.has_more, next_cursor: resp?.next_cursor });
506
+ },
507
+ },
508
+ {
509
+ name: 'list_all_workspace_databases',
510
+ description:
511
+ 'Lists all databases accessible to this integration in the Notion workspace. Returns database IDs, names and property schemas. Use to discover available databases before querying.',
512
+ inputSchema: {
513
+ type: 'object',
514
+ properties: {
515
+ page_size: { type: 'number', description: 'Page size. Defaults to 50.' },
516
+ start_cursor: { type: 'string', description: 'Pagination cursor.' },
517
+ },
518
+ required: [],
519
+ },
520
+ handler: async ({ page_size = 50, start_cursor }) => {
521
+ const resp = await call('POST', '/v1/search', {
522
+ _rawBody: { page_size, start_cursor, filter: { property: 'object', value: 'database' } },
523
+ });
524
+ const out = (resp?.results || []).map((x) => ({ id: x.id, title: (x.title || []).map((t) => t.plain_text).join(''), properties: x.properties }));
525
+ return text({ results: out, has_more: resp?.has_more, next_cursor: resp?.next_cursor });
526
+ },
527
+ },
528
+ {
529
+ name: 'add_comment_to_page',
530
+ description:
531
+ 'Adds a top-level comment to a Notion page. Use for adding review notes, feedback or status updates to a page. For replying to existing discussion threads use reply_to_comment.',
532
+ inputSchema: {
533
+ type: 'object',
534
+ properties: {
535
+ page_id: { type: 'string', description: 'Target page ID.' },
536
+ comment_text: { type: 'string', description: 'Comment text.' },
537
+ },
538
+ required: ['page_id', 'comment_text'],
539
+ },
540
+ handler: async ({ page_id, comment_text }) =>
541
+ text(
542
+ await call('POST', '/v1/comments', {
543
+ _rawBody: { parent: { page_id }, rich_text: richText(comment_text) },
544
+ })
545
+ ),
546
+ },
547
+ {
548
+ name: 'reply_to_comment',
549
+ description:
550
+ 'Replies to an existing comment thread on a Notion page. Requires the discussion_id from an existing comment. Use to continue a conversation thread. For new top-level comments use add_comment_to_page.',
551
+ inputSchema: {
552
+ type: 'object',
553
+ properties: {
554
+ discussion_id: { type: 'string', description: 'Discussion thread ID.' },
555
+ reply_text: { type: 'string', description: 'Reply text.' },
556
+ },
557
+ required: ['discussion_id', 'reply_text'],
558
+ },
559
+ handler: async ({ discussion_id, reply_text }) =>
560
+ text(
561
+ await call('POST', '/v1/comments', {
562
+ _rawBody: { discussion_id, rich_text: richText(reply_text) },
563
+ })
564
+ ),
565
+ },
566
+ ];
567
+
568
+ export const tools = [...baseTools, ...smartTools];
@@ -0,0 +1,8 @@
1
+ SALESFORCE_CLIENT_ID=fake_client_id_for_testing
2
+ SALESFORCE_CLIENT_SECRET=fake_client_secret
3
+ SALESFORCE_USERNAME=test@test.com
4
+ SALESFORCE_PASSWORD=fakepassword
5
+ SALESFORCE_SECURITY_TOKEN=faketoken123
6
+ SALESFORCE_LOGIN_URL=https://login.salesforce.com
7
+ SALESFORCE_API_VERSION=v59.0
8
+ SALESFORCE_INSTANCE_URL=https://fake.salesforce.com
@@ -0,0 +1,15 @@
1
+ # Connected App (required for npx adoptai-salesforce-mcp setup OAuth)
2
+ SALESFORCE_CLIENT_ID=
3
+ SALESFORCE_CLIENT_SECRET=
4
+
5
+ # Production login; use https://test.salesforce.com for sandboxes
6
+ SALESFORCE_LOGIN_URL=https://login.salesforce.com
7
+
8
+ # Optional: fixed OAuth callback (must match Connected App redirect URL)
9
+ # SALESFORCE_REDIRECT_URI=http://127.0.0.1:8765/callback
10
+
11
+ # Optional API version for tools (default v59.0). MCP config usually sets this after OAuth.
12
+ # SALESFORCE_API_VERSION=v59.0
13
+
14
+ # Optional: instance URL if not injected by MCP env (normally set automatically at setup)
15
+ # SALESFORCE_INSTANCE_URL=https://yourinstance.salesforce.com