@jskit-ai/connectors-catalog 0.1.1

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 (399) hide show
  1. package/README.md +192 -0
  2. package/docs/ai-upstream-notices.md +51 -0
  3. package/docs/ai.md +191 -0
  4. package/docs/airtable.md +130 -0
  5. package/docs/algolia.md +190 -0
  6. package/docs/amazon-redshift.md +229 -0
  7. package/docs/amplitude.md +210 -0
  8. package/docs/apify.md +162 -0
  9. package/docs/apollo-io.md +200 -0
  10. package/docs/asana.md +126 -0
  11. package/docs/ashby.md +188 -0
  12. package/docs/atlassian.md +224 -0
  13. package/docs/attention.md +162 -0
  14. package/docs/aws-athena.md +150 -0
  15. package/docs/aws-credentials.md +75 -0
  16. package/docs/aws-s3.md +193 -0
  17. package/docs/bigquery.md +146 -0
  18. package/docs/brevo.md +122 -0
  19. package/docs/calendly.md +158 -0
  20. package/docs/canva.md +191 -0
  21. package/docs/chargebee.md +192 -0
  22. package/docs/clay.md +194 -0
  23. package/docs/clickhouse.md +242 -0
  24. package/docs/confidence-exp.md +74 -0
  25. package/docs/confidence-flags.md +75 -0
  26. package/docs/confidence-oauth.md +137 -0
  27. package/docs/contentful.md +123 -0
  28. package/docs/databricks.md +180 -0
  29. package/docs/dbt-semantic-layer.md +175 -0
  30. package/docs/elevenlabs.md +102 -0
  31. package/docs/figma.md +132 -0
  32. package/docs/firebase-cloud-messaging.md +262 -0
  33. package/docs/firecrawl.md +137 -0
  34. package/docs/fireflies.md +95 -0
  35. package/docs/fireworks-ai.md +53 -0
  36. package/docs/gatewayapi.md +113 -0
  37. package/docs/gemini-enterprise.md +224 -0
  38. package/docs/github-api.md +133 -0
  39. package/docs/gitlab-api.md +138 -0
  40. package/docs/gmail.md +133 -0
  41. package/docs/gong.md +205 -0
  42. package/docs/google-ads.md +156 -0
  43. package/docs/google-analytics.md +202 -0
  44. package/docs/google-docs.md +105 -0
  45. package/docs/google-drive.md +108 -0
  46. package/docs/google-maps-platform.md +208 -0
  47. package/docs/google-oauth.md +123 -0
  48. package/docs/google-search-console.md +124 -0
  49. package/docs/google-sheets.md +94 -0
  50. package/docs/google-slides.md +95 -0
  51. package/docs/granola.md +178 -0
  52. package/docs/hex.md +161 -0
  53. package/docs/heygen.md +124 -0
  54. package/docs/hubspot.md +142 -0
  55. package/docs/incident-io.md +118 -0
  56. package/docs/inngest.md +178 -0
  57. package/docs/klipy.md +149 -0
  58. package/docs/lexware.md +144 -0
  59. package/docs/lightspeed.md +193 -0
  60. package/docs/linear.md +137 -0
  61. package/docs/linkedin.md +196 -0
  62. package/docs/logo-dev.md +152 -0
  63. package/docs/mailgun.md +153 -0
  64. package/docs/mapbox.md +174 -0
  65. package/docs/microsoft-excel.md +97 -0
  66. package/docs/microsoft-fabric.md +249 -0
  67. package/docs/microsoft-oauth.md +143 -0
  68. package/docs/microsoft-onedrive.md +76 -0
  69. package/docs/microsoft-onenote.md +75 -0
  70. package/docs/microsoft-outlook.md +90 -0
  71. package/docs/microsoft-powerpoint.md +119 -0
  72. package/docs/microsoft-sharepoint.md +75 -0
  73. package/docs/microsoft-teams.md +72 -0
  74. package/docs/microsoft-word.md +119 -0
  75. package/docs/miro.md +117 -0
  76. package/docs/n8n.md +169 -0
  77. package/docs/notion.md +137 -0
  78. package/docs/oura.md +161 -0
  79. package/docs/paddle.md +168 -0
  80. package/docs/perplexity.md +85 -0
  81. package/docs/pipedrive.md +120 -0
  82. package/docs/polar.md +63 -0
  83. package/docs/posthog.md +227 -0
  84. package/docs/prestashop.md +151 -0
  85. package/docs/replicate.md +60 -0
  86. package/docs/resend.md +153 -0
  87. package/docs/salesforce.md +234 -0
  88. package/docs/sanity.md +122 -0
  89. package/docs/semrush.md +276 -0
  90. package/docs/sentry.md +112 -0
  91. package/docs/sevdesk.md +107 -0
  92. package/docs/shopify.md +206 -0
  93. package/docs/slack.md +257 -0
  94. package/docs/snowflake.md +271 -0
  95. package/docs/storyblok.md +130 -0
  96. package/docs/stripe.md +77 -0
  97. package/docs/supabase.md +51 -0
  98. package/docs/tally.md +105 -0
  99. package/docs/telegram.md +165 -0
  100. package/docs/tiktok.md +201 -0
  101. package/docs/twilio.md +284 -0
  102. package/docs/twitch.md +428 -0
  103. package/docs/wave.md +440 -0
  104. package/docs/wix.md +1249 -0
  105. package/docs/wiz.md +177 -0
  106. package/docs/woocommerce.md +400 -0
  107. package/docs/wordpress-com.md +358 -0
  108. package/docs/wordpress-self-hosted.md +252 -0
  109. package/docs/workday.md +292 -0
  110. package/docs/x-twitter.md +188 -0
  111. package/docs/xero.md +429 -0
  112. package/docs/zoho-books.md +214 -0
  113. package/docs/zoho-crm.md +192 -0
  114. package/package.json +153 -0
  115. package/patterns/ai-connections/PATTERN.md +84 -0
  116. package/patterns/ai-connections/example/ai-model.js +11 -0
  117. package/patterns/ai-connections/example/perplexity-answer.js +29 -0
  118. package/patterns/api-key-connection/PATTERN.md +368 -0
  119. package/patterns/api-key-connection/example/connections.js +18 -0
  120. package/patterns/api-key-connection/example/integrations.json +19 -0
  121. package/patterns/assistant-mcp/PATTERN.md +110 -0
  122. package/patterns/assistant-mcp/example/integrations.json +13 -0
  123. package/patterns/assistant-mcp-oauth/PATTERN.md +235 -0
  124. package/patterns/assistant-mcp-oauth/example/integrations.json +102 -0
  125. package/patterns/aws-storage-queries/PATTERN.md +169 -0
  126. package/patterns/aws-storage-queries/example/formats/data-formats.js +35 -0
  127. package/patterns/aws-storage-queries/example/formats/package-lock.json +49 -0
  128. package/patterns/aws-storage-queries/example/formats/package.json +12 -0
  129. package/patterns/aws-storage-queries/example/formats/verify-formats.mjs +37 -0
  130. package/patterns/aws-storage-queries/example/integrations.json +38 -0
  131. package/patterns/aws-storage-queries/example/s3-transfer.js +34 -0
  132. package/patterns/event-delivery/PATTERN.md +151 -0
  133. package/patterns/event-delivery/example/integrations.json +17 -0
  134. package/patterns/firebase-messaging/PATTERN.md +156 -0
  135. package/patterns/firebase-messaging/example/server/notifications.js +23 -0
  136. package/patterns/google-ads-search/PATTERN.md +92 -0
  137. package/patterns/google-ads-search/example/ads-setup.js +25 -0
  138. package/patterns/oauth-connection/PATTERN.md +724 -0
  139. package/patterns/oauth-connection/example/integrations.json +20 -0
  140. package/patterns/paddle-catalogue/PATTERN.md +78 -0
  141. package/patterns/paddle-catalogue/example/create-products.js +38 -0
  142. package/patterns/public-image/PATTERN.md +84 -0
  143. package/patterns/public-image/example/integrations.json +13 -0
  144. package/patterns/public-image/example/logo-url.js +16 -0
  145. package/patterns/redshift-queries/PATTERN.md +150 -0
  146. package/patterns/redshift-queries/example/integrations.json +33 -0
  147. package/patterns/source-scanning/PATTERN.md +83 -0
  148. package/patterns/source-scanning/example/source-scanner.js +13 -0
  149. package/src/client/firebase-cloud-messaging.js +10 -0
  150. package/src/client/google-analytics.js +72 -0
  151. package/src/client/google-maps-platform.js +39 -0
  152. package/src/client/logo-dev.js +36 -0
  153. package/src/server/ai.js +46 -0
  154. package/src/server/airtable.js +67 -0
  155. package/src/server/algolia.js +63 -0
  156. package/src/server/amazon-redshift.js +96 -0
  157. package/src/server/amplitude.js +31 -0
  158. package/src/server/apify.js +68 -0
  159. package/src/server/apollo-io.js +85 -0
  160. package/src/server/asana.js +64 -0
  161. package/src/server/ashby.js +61 -0
  162. package/src/server/atlassian.js +32 -0
  163. package/src/server/attention.js +93 -0
  164. package/src/server/aws-athena.js +82 -0
  165. package/src/server/aws-s3.js +49 -0
  166. package/src/server/awsTransport.js +75 -0
  167. package/src/server/bigquery.js +97 -0
  168. package/src/server/brevo.js +97 -0
  169. package/src/server/calendly.js +77 -0
  170. package/src/server/canva.js +13 -0
  171. package/src/server/chargebee.js +88 -0
  172. package/src/server/clay.js +78 -0
  173. package/src/server/clickhouse.js +66 -0
  174. package/src/server/confidence.js +29 -0
  175. package/src/server/contentful.js +40 -0
  176. package/src/server/databricks.js +55 -0
  177. package/src/server/dbt-semantic-layer.js +95 -0
  178. package/src/server/elevenlabs.js +81 -0
  179. package/src/server/figma.js +24 -0
  180. package/src/server/firebase-cloud-messaging.js +111 -0
  181. package/src/server/firecrawl.js +70 -0
  182. package/src/server/fireflies.js +31 -0
  183. package/src/server/fireworks-ai.js +16 -0
  184. package/src/server/gatewayapi.js +48 -0
  185. package/src/server/gemini-enterprise.js +47 -0
  186. package/src/server/github-api.js +90 -0
  187. package/src/server/gitlab-api.js +67 -0
  188. package/src/server/gmail.js +60 -0
  189. package/src/server/gong.js +52 -0
  190. package/src/server/google-ads-search.js +134 -0
  191. package/src/server/google-ads.js +80 -0
  192. package/src/server/google-docs.js +36 -0
  193. package/src/server/google-drive.js +71 -0
  194. package/src/server/google-maps-platform.js +90 -0
  195. package/src/server/google-search-console.js +31 -0
  196. package/src/server/google-sheets.js +60 -0
  197. package/src/server/google-slides.js +36 -0
  198. package/src/server/google.js +61 -0
  199. package/src/server/granola.js +115 -0
  200. package/src/server/graphqlOperation.js +15 -0
  201. package/src/server/hex.js +29 -0
  202. package/src/server/heygen.js +54 -0
  203. package/src/server/hubspot.js +85 -0
  204. package/src/server/incident-io.js +68 -0
  205. package/src/server/inngest.js +86 -0
  206. package/src/server/jsonOperation.js +34 -0
  207. package/src/server/klipy.js +34 -0
  208. package/src/server/lexware.js +59 -0
  209. package/src/server/lightspeed.js +82 -0
  210. package/src/server/linear.js +52 -0
  211. package/src/server/linkedin.js +74 -0
  212. package/src/server/mailgun.js +76 -0
  213. package/src/server/mapbox.js +46 -0
  214. package/src/server/mcpRegistration.js +41 -0
  215. package/src/server/mcpTransport.js +60 -0
  216. package/src/server/microsoft-excel.js +67 -0
  217. package/src/server/microsoft-fabric.js +81 -0
  218. package/src/server/microsoft-onedrive.js +58 -0
  219. package/src/server/microsoft-onenote.js +52 -0
  220. package/src/server/microsoft-outlook.js +58 -0
  221. package/src/server/microsoft-powerpoint.js +5 -0
  222. package/src/server/microsoft-sharepoint.js +46 -0
  223. package/src/server/microsoft-teams.js +29 -0
  224. package/src/server/microsoft-word.js +5 -0
  225. package/src/server/microsoft.js +88 -0
  226. package/src/server/miro.js +29 -0
  227. package/src/server/n8n.js +78 -0
  228. package/src/server/notion.js +76 -0
  229. package/src/server/oura.js +48 -0
  230. package/src/server/paddle.js +83 -0
  231. package/src/server/perplexity.js +15 -0
  232. package/src/server/pipedrive.js +95 -0
  233. package/src/server/polar.js +20 -0
  234. package/src/server/posthog.js +44 -0
  235. package/src/server/prestashop.js +42 -0
  236. package/src/server/replicate.js +42 -0
  237. package/src/server/resend.js +93 -0
  238. package/src/server/salesforce.js +84 -0
  239. package/src/server/sanity.js +31 -0
  240. package/src/server/semrush.js +359 -0
  241. package/src/server/sentry.js +30 -0
  242. package/src/server/sevdesk.js +20 -0
  243. package/src/server/shopify.js +99 -0
  244. package/src/server/slack.js +143 -0
  245. package/src/server/snowflake.js +139 -0
  246. package/src/server/storyblok.js +49 -0
  247. package/src/server/stripe.js +13 -0
  248. package/src/server/supabase.js +13 -0
  249. package/src/server/tally.js +78 -0
  250. package/src/server/telegram.js +55 -0
  251. package/src/server/tiktok.js +86 -0
  252. package/src/server/twilio.js +90 -0
  253. package/src/server/twitch.js +409 -0
  254. package/src/server/wave.js +520 -0
  255. package/src/server/wix.js +1048 -0
  256. package/src/server/wiz.js +117 -0
  257. package/src/server/woocommerce.js +412 -0
  258. package/src/server/wordpress-com.js +236 -0
  259. package/src/server/wordpress-self-hosted.js +134 -0
  260. package/src/server/workday.js +136 -0
  261. package/src/server/x-twitter.js +110 -0
  262. package/src/server/xero.js +397 -0
  263. package/src/server/zoho-books.js +90 -0
  264. package/src/server/zoho-crm.js +89 -0
  265. package/src/server/zohoOAuth.js +21 -0
  266. package/src/shared/ai-models.json +8049 -0
  267. package/src/shared/ai.js +63 -0
  268. package/src/shared/algolia.js +29 -0
  269. package/src/shared/amazon-redshift.js +51 -0
  270. package/src/shared/amplitude.js +28 -0
  271. package/src/shared/atlassian.js +43 -0
  272. package/src/shared/aws.js +72 -0
  273. package/src/shared/canva.js +66 -0
  274. package/src/shared/chargebee.js +26 -0
  275. package/src/shared/clickhouse.js +38 -0
  276. package/src/shared/confidence.js +28 -0
  277. package/src/shared/databricks.js +33 -0
  278. package/src/shared/dbt-semantic-layer.js +32 -0
  279. package/src/shared/definitions.js +140 -0
  280. package/src/shared/figma.js +21 -0
  281. package/src/shared/firebase-cloud-messaging.js +45 -0
  282. package/src/shared/gemini-enterprise.js +33 -0
  283. package/src/shared/gong.js +33 -0
  284. package/src/shared/google-ads-search.js +31 -0
  285. package/src/shared/google-ads.js +28 -0
  286. package/src/shared/google-maps-platform.js +28 -0
  287. package/src/shared/google.js +133 -0
  288. package/src/shared/granola.js +25 -0
  289. package/src/shared/hex.js +28 -0
  290. package/src/shared/inngest.js +35 -0
  291. package/src/shared/lightspeed.js +75 -0
  292. package/src/shared/linkedin.js +27 -0
  293. package/src/shared/logo-dev.js +21 -0
  294. package/src/shared/mapbox.js +38 -0
  295. package/src/shared/mcp.js +92 -0
  296. package/src/shared/microsoft-fabric.js +47 -0
  297. package/src/shared/microsoft.js +231 -0
  298. package/src/shared/miro.js +27 -0
  299. package/src/shared/oura.js +23 -0
  300. package/src/shared/posthog.js +33 -0
  301. package/src/shared/prestashop.js +23 -0
  302. package/src/shared/salesforce.js +36 -0
  303. package/src/shared/semrush.js +24 -0
  304. package/src/shared/shopify.js +51 -0
  305. package/src/shared/siteUrl.js +12 -0
  306. package/src/shared/slack.js +88 -0
  307. package/src/shared/snowflake.js +55 -0
  308. package/src/shared/tiktok.js +22 -0
  309. package/src/shared/tokens.js +747 -0
  310. package/src/shared/twilio.js +41 -0
  311. package/src/shared/twitch.js +49 -0
  312. package/src/shared/wave.js +52 -0
  313. package/src/shared/wix.js +40 -0
  314. package/src/shared/wiz.js +41 -0
  315. package/src/shared/wordpress-com.js +30 -0
  316. package/src/shared/wordpress.js +69 -0
  317. package/src/shared/workday.js +50 -0
  318. package/src/shared/x-twitter.js +19 -0
  319. package/src/shared/xero.js +46 -0
  320. package/src/shared/zoho-books.js +39 -0
  321. package/src/shared/zoho-crm.js +43 -0
  322. package/src/shared/zoho-regions.js +12 -0
  323. package/test/ai.test.js +136 -0
  324. package/test/airtable.test.js +125 -0
  325. package/test/algolia.test.js +207 -0
  326. package/test/amazon-redshift.test.js +272 -0
  327. package/test/amplitude.test.js +239 -0
  328. package/test/apify.test.js +93 -0
  329. package/test/apollo-io.test.js +125 -0
  330. package/test/asana.test.js +99 -0
  331. package/test/ashby.test.js +97 -0
  332. package/test/atlassian.test.js +249 -0
  333. package/test/attention.test.js +79 -0
  334. package/test/aws.test.js +405 -0
  335. package/test/brevo.test.js +71 -0
  336. package/test/calendly.test.js +136 -0
  337. package/test/canva.test.js +258 -0
  338. package/test/chargebee.test.js +195 -0
  339. package/test/clickhouse.test.js +278 -0
  340. package/test/confidence.test.js +205 -0
  341. package/test/databricks.test.js +253 -0
  342. package/test/dbt-semantic-layer.test.js +221 -0
  343. package/test/designMcp.test.js +336 -0
  344. package/test/elevenlabs.test.js +52 -0
  345. package/test/firebase-cloud-messaging.test.js +242 -0
  346. package/test/firecrawl.test.js +66 -0
  347. package/test/gemini-enterprise.test.js +218 -0
  348. package/test/github-api.test.js +167 -0
  349. package/test/gitlab-api.test.js +160 -0
  350. package/test/gong.test.js +192 -0
  351. package/test/google-ads.test.js +350 -0
  352. package/test/google-maps-platform.test.js +191 -0
  353. package/test/google.test.js +710 -0
  354. package/test/granola.test.js +240 -0
  355. package/test/hex.test.js +253 -0
  356. package/test/heygen.test.js +121 -0
  357. package/test/hubspot.test.js +103 -0
  358. package/test/inngest.test.js +219 -0
  359. package/test/lightspeed.test.js +250 -0
  360. package/test/linear.test.js +146 -0
  361. package/test/linkedin.test.js +243 -0
  362. package/test/logo-dev.test.js +79 -0
  363. package/test/mailgun.test.js +107 -0
  364. package/test/mapbox.test.js +181 -0
  365. package/test/mcp.test.js +227 -0
  366. package/test/microsoft-fabric.test.js +258 -0
  367. package/test/microsoft.test.js +554 -0
  368. package/test/n8nOAuth.test.js +188 -0
  369. package/test/notionOAuth.test.js +183 -0
  370. package/test/oura.test.js +211 -0
  371. package/test/paddleCatalogue.test.js +51 -0
  372. package/test/paddlePermissions.test.js +101 -0
  373. package/test/perplexityComposition.test.js +39 -0
  374. package/test/pipedriveOAuth.test.js +140 -0
  375. package/test/posthog.test.js +200 -0
  376. package/test/prestashop.test.js +207 -0
  377. package/test/resend.test.js +163 -0
  378. package/test/salesforce.test.js +276 -0
  379. package/test/semrush.test.js +479 -0
  380. package/test/sentry.test.js +35 -0
  381. package/test/shopify.test.js +247 -0
  382. package/test/slack.test.js +320 -0
  383. package/test/snowflake.test.js +391 -0
  384. package/test/tally.test.js +75 -0
  385. package/test/telegram.test.js +58 -0
  386. package/test/tiktok.test.js +243 -0
  387. package/test/tokens.test.js +1079 -0
  388. package/test/twilio.test.js +221 -0
  389. package/test/twitch.test.js +573 -0
  390. package/test/wave.test.js +678 -0
  391. package/test/wix.test.js +1099 -0
  392. package/test/wiz.test.js +185 -0
  393. package/test/wordpress-com.test.js +469 -0
  394. package/test/wordpress.test.js +660 -0
  395. package/test/workday.test.js +314 -0
  396. package/test/x-twitter.test.js +256 -0
  397. package/test/xero.test.js +700 -0
  398. package/test/zoho-books.test.js +253 -0
  399. package/test/zoho-crm.test.js +223 -0
@@ -0,0 +1,84 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { salesforceDefinition } from "../shared/salesforce.js";
5
+ import { jsonOperation } from "./jsonOperation.js";
6
+
7
+ const apiPath = "/services/data/v66.0";
8
+ const origin = (settings) => new URL(settings.accountUrl).origin;
9
+ const endpoint = (resource) => (settings) => `${origin(settings)}${apiPath}/${resource}`;
10
+ const objectName = (value) => typeof value === "string" && /^[A-Za-z][A-Za-z0-9_]{0,254}$/u.test(value);
11
+ const locator = (value) => typeof value === "string" && /^\/services\/data\/v66\.0\/query\/[A-Za-z0-9]{15,18}-[0-9]{1,10}$/u.test(value);
12
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
13
+ const limits = (value) => record(value) && Number.isSafeInteger(value.DailyApiRequests?.Max) && value.DailyApiRequests.Max >= 0 &&
14
+ Number.isSafeInteger(value.DailyApiRequests.Remaining) && value.DailyApiRequests.Remaining >= 0;
15
+ const queryResult = (value) => Number.isSafeInteger(value?.totalSize) && value.totalSize >= 0 && typeof value.done === "boolean" &&
16
+ Array.isArray(value.records) && value.records.length <= 2000 && value.records.every(record) && value.records.length <= value.totalSize &&
17
+ (value.done ? value.nextRecordsUrl == null : locator(value.nextRecordsUrl));
18
+ const describeSchema = createSchema({ object: { type: "string", required: true, validator: (value) => objectName(value) || "Use a Salesforce object API name." } });
19
+ const nextSchema = createSchema({ nextRecordsUrl: { type: "string", required: true, noTrim: true,
20
+ validator: (value) => locator(value) || "Use the nextRecordsUrl returned by this connection's v66.0 query." } });
21
+
22
+ const salesforceProvider = Object.freeze({
23
+ ...salesforceDefinition,
24
+ oauth: (settings) => ({ issuer: origin(settings), authorization_endpoint: `${origin(settings)}/services/oauth2/authorize`, token_endpoint: `${origin(settings)}/services/oauth2/token` }),
25
+ apiOrigins: (settings) => [origin(settings)], checkOperation: "limits.read",
26
+ async normalizeTokenResponse(response, { settings }) {
27
+ if (!response.ok) return response;
28
+ let value;
29
+ try { value = await response.clone().json(); } catch { /* reported below */ }
30
+ if (!record(value) || ![origin(settings), origin(settings) + "/"].includes(value.instance_url) || typeof value.scope !== "string" || !value.scope.trim()) {
31
+ throw new ConnectorError("connector_response_invalid", "Salesforce returned an invalid grant or a different organisation.", { statusCode: 502 });
32
+ }
33
+ // Salesforce can omit expiry. Renew after five minutes of use as a local
34
+ // policy; this is not a claim about the org's actual session lifetime.
35
+ return value.expires_in === undefined ? Response.json({ ...value, expires_in: 300 }) : response;
36
+ },
37
+ operations: {
38
+ "limits.read": { ...jsonOperation(endpoint("limits"), {}, limits), scopes: ["api"] },
39
+ "objects.list": { ...jsonOperation(endpoint("sobjects"), {},
40
+ (value) => Array.isArray(value?.sobjects) && value.sobjects.every((object) => objectName(object?.name) && typeof object.label === "string" && typeof object.queryable === "boolean")), scopes: ["api"] },
41
+ "objects.describe": {
42
+ scopes: ["api"],
43
+ request(input, settings) {
44
+ const values = validateSchemaPayload({ schema: describeSchema, mode: "replace" }, input, { statusCode: 422 });
45
+ return { method: "GET", url: endpoint(`sobjects/${values.object}/describe`)(settings) };
46
+ },
47
+ validateResult: (value) => objectName(value?.name) && Array.isArray(value.fields) && value.fields.every((field) => objectName(field?.name) && typeof field.type === "string")
48
+ },
49
+ "query.read": {
50
+ ...jsonOperation(endpoint("query"), { q: { type: "string", required: true, minLength: 1, maxLength: 2000,
51
+ validator: (value) => /^SELECT\s/iu.test(value) && new TextEncoder().encode(value).length <= 3000 && !/\bFOR\s+(?:UPDATE|VIEW|REFERENCE)\b/iu.test(value) && !/[\p{Cc}]/u.test(value) || "Use a single-line SELECT query without FOR UPDATE, FOR VIEW or FOR REFERENCE." } }, queryResult), scopes: ["api"]
52
+ },
53
+ "query.next": {
54
+ scopes: ["api"],
55
+ request(input, settings) {
56
+ const values = validateSchemaPayload({ schema: nextSchema, mode: "replace" }, input, { statusCode: 422 });
57
+ return { method: "GET", url: origin(settings) + values.nextRecordsUrl };
58
+ },
59
+ validateResult: queryResult
60
+ }
61
+ },
62
+ async exchange(address, options, { fetchImpl }) {
63
+ const response = await fetchImpl(address, { ...options, credentials: "omit" });
64
+ let value;
65
+ try { value = await response.json(); } catch { /* checked after status */ }
66
+ const code = Array.isArray(value) ? value[0]?.errorCode : null;
67
+ if (!response.ok) {
68
+ if (code === "REQUEST_LIMIT_EXCEEDED") throw new ConnectorError("connector_rate_limited", "The Salesforce organisation's API request limit was reached.", { statusCode: 429 });
69
+ if (code === "INVALID_SESSION_ID") throw new ConnectorError("connector_reconnect_required", "Connect this Salesforce account again.", { statusCode: 401 });
70
+ if (code === "INVALID_QUERY_LOCATOR") throw new ConnectorError("connector_cursor_expired", "Start the Salesforce query again; its result cursor is no longer valid.", { statusCode: 410 });
71
+ if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This Salesforce resource is not available.", { statusCode: 404 });
72
+ if (response.status === 400) throw new ConnectorError("connector_query_invalid", "Salesforce rejected the query or object. Check its API names and the user's permissions.", { statusCode: 422 });
73
+ throw Object.assign(new Error("Salesforce request failed."), { status: response.status });
74
+ }
75
+ if (response.status !== 200 || !record(value)) throw new ConnectorError("connector_response_invalid", "Salesforce returned an unexpected response.", { statusCode: 502 });
76
+ const url = new URL(address);
77
+ if (url.pathname.endsWith("/describe") && value.name !== url.pathname.split("/").at(-2)) {
78
+ throw new ConnectorError("connector_response_invalid", "Salesforce described a different object.", { statusCode: 502 });
79
+ }
80
+ return value;
81
+ }
82
+ });
83
+
84
+ export { salesforceProvider };
@@ -0,0 +1,31 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { mcpRegistrationFields, registerMcpClient } from "./mcpRegistration.js";
4
+ import { sanityDefinition } from "../shared/mcp.js";
5
+ import { mcpOperations, exchangeMcp } from "./mcpTransport.js";
6
+
7
+ const oauth = Object.freeze({
8
+ issuer: "https://mcp.sanity.io", authorization_endpoint: "https://mcp.sanity.io/authorize",
9
+ token_endpoint: "https://mcp.sanity.io/token", registration_endpoint: "https://mcp.sanity.io/register",
10
+ response_types_supported: ["code"], code_challenge_methods_supported: ["S256"]
11
+ });
12
+ const registrationSchema = createSchema({ ...mcpRegistrationFields,
13
+ scopes: { type: "array", required: true, minLength: 1,
14
+ items: { type: "string", enum: ["global"] },
15
+ validator: (values) => values.length === 1 && values[0] === "global" || "Select Sanity MCP access." }
16
+ });
17
+ async function registerSanityClient(input, options) {
18
+ const values = validateSchemaPayload({ schema: registrationSchema, mode: "replace" }, input, { statusCode: 422 });
19
+ return registerMcpClient("Sanity", oauth, values, options);
20
+ }
21
+
22
+ const sanityProvider = Object.freeze({
23
+ ...sanityDefinition, oauth, oauthResource: "https://mcp.sanity.io",
24
+ apiOrigins: ["https://mcp.sanity.io"],
25
+ apiKey: { headers: (token) => ({ Authorization: `Bearer ${token}` }) },
26
+ checkOperation: "tools.list",
27
+ exchange: exchangeMcp,
28
+ operations: mcpOperations("https://mcp.sanity.io/")
29
+ });
30
+
31
+ export { sanityProvider, registerSanityClient };
@@ -0,0 +1,359 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { semrushDefinition } from "../shared/semrush.js";
5
+ import { jsonOperation } from "./jsonOperation.js";
6
+
7
+ const projectsUrl = "https://api.semrush.com/apis/v4/projects/v1/projects";
8
+ const projectSchema = createSchema({ projectId: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER } });
9
+ const projectName = { type: "string", required: true, minLength: 1,
10
+ pattern: /^[^~`!#%'\^&*=\[\]\\/{}|":<>?\p{Cc}]+$/u };
11
+ const renameSchema = createSchema({
12
+ projectId: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER },
13
+ project_name: projectName
14
+ });
15
+
16
+ const backlinkTarget = {
17
+ url: { type: "string", required: true, minLength: 1, maxLength: 2000 },
18
+ scope: { type: "string", required: true, enum: ["ROOT_DOMAIN", "SUBDOMAIN", "SUBFOLDER", "PAGE"] },
19
+ format: { type: "string", enum: ["json"], defaultTo: "json" }
20
+ };
21
+ const backlinkPage = {
22
+ ...backlinkTarget,
23
+ limit: { type: "integer", min: 1, max: 1000, defaultTo: 100 },
24
+ offset: { type: "integer", min: 0, max: Number.MAX_SAFE_INTEGER, defaultTo: 0 },
25
+ order_by: { type: "string", pattern: /^[a-z_]+$/u },
26
+ direction: { type: "string", enum: ["ASC", "DESC"], defaultTo: "DESC" },
27
+ filter: { type: "string", minLength: 1, maxLength: 4000 }
28
+ };
29
+
30
+ const domainColumns = ["Domain", "Rank", "Organic Keywords", "Organic Traffic", "Organic Cost", "Adwords Keywords", "Adwords Traffic", "Adwords Cost"];
31
+
32
+ const keywordColumns = ["Keyword", "Position", "Search Volume", "CPC", "Competition", "Traffic (%)", "Number of Results"];
33
+
34
+ function semrushKeywordReport(target, kind) {
35
+ const operation = jsonOperation("https://api.semrush.com/", {
36
+ [target]: { type: "string", required: true, minLength: 1, maxLength: target === "domain" ? 253 : 2000 },
37
+ database: { type: "string", required: true, pattern: /^[a-z]{2}$/u },
38
+ limit: { type: "integer", min: 1, max: 1000, defaultTo: 100 },
39
+ offset: { type: "integer", min: 0, max: 3_999_999, defaultTo: 0 },
40
+ display_sort: { type: "string", enum: ["po_asc", "po_desc", "tr_asc", "tr_desc", "nq_asc", "nq_desc"] },
41
+ display_filter: { type: "string", minLength: 1, maxLength: 4000 },
42
+ display_date: { type: "string", pattern: /^\d{4}(0[1-9]|1[0-2])15$/u }
43
+ });
44
+ return {
45
+ scopes: [],
46
+ request(input, settings) {
47
+ if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the separate Semrush V3 API key reference for keyword reports.");
48
+ const result = operation.request(input);
49
+ const url = new URL(result.url);
50
+ const limit = Number(url.searchParams.get("limit")), offset = Number(url.searchParams.get("offset"));
51
+ if (limit + offset > 4_000_000) throw new ConnectorError("connector_input_invalid", "The requested Semrush page exceeds the provider's result window.", { statusCode: 422 });
52
+ url.searchParams.delete("limit"); url.searchParams.delete("offset");
53
+ url.searchParams.set("display_limit", String(limit + offset));
54
+ url.searchParams.set("display_offset", String(offset));
55
+ url.searchParams.set("type", `${target}_${kind}`);
56
+ url.searchParams.set("export_columns", "Ph,Po,Nq,Cp,Co,Tr,Nr");
57
+ url.searchParams.set("export_escape", "1"); url.searchParams.set("export_decode", "1");
58
+ return { ...result, url: url.href };
59
+ }
60
+ };
61
+ }
62
+
63
+ function semrushTrackingReport(type, positions = false) {
64
+ const operation = jsonOperation("https://api.semrush.com/", {
65
+ campaignId: { type: "string", required: true, pattern: /^\d+(?:_\d+)?$/u, maxLength: 80 },
66
+ ...(positions ? {
67
+ url: { type: "string", minLength: 1, maxLength: 2000 },
68
+ date_begin: { type: "string", pattern: /^\d{4}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])$/u },
69
+ date_end: { type: "string", pattern: /^\d{4}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])$/u },
70
+ display_limit: { type: "integer", min: 1, max: 1000, defaultTo: 10 },
71
+ display_offset: { type: "integer", min: 0, max: Number.MAX_SAFE_INTEGER, defaultTo: 0 },
72
+ display_filter: { type: "string", minLength: 1, maxLength: 4000 }
73
+ } : {})
74
+ });
75
+ return { scopes: [], request(input, settings) {
76
+ if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the Semrush V3 key for position tracking.");
77
+ const result = operation.request(input);
78
+ const url = new URL(result.url);
79
+ url.pathname = `/reports/v1/projects/${url.searchParams.get("campaignId")}/tracking/`;
80
+ url.searchParams.delete("campaignId");
81
+ if (url.searchParams.has("date_begin") && url.searchParams.has("date_end") && url.searchParams.get("date_begin") > url.searchParams.get("date_end")) {
82
+ throw new ConnectorError("connector_input_invalid", "The tracking start date must not follow its end date.", { statusCode: 422 });
83
+ }
84
+ url.searchParams.set("action", "report"); url.searchParams.set("type", type);
85
+ return { ...result, url: url.href };
86
+ } };
87
+ }
88
+
89
+ function parseSemrushCsv(text) {
90
+ const rows = [];
91
+ let row = [], field = "", quoted = false, closed = false;
92
+ for (let i = 0; i < text.length; i++) {
93
+ const char = text[i];
94
+ if (quoted) {
95
+ if (char === '"' && text[i + 1] === '"') { field += '"'; i++; }
96
+ else if (char === '"') { quoted = false; closed = true; }
97
+ else field += char;
98
+ } else if (char === '"' && field === "" && !closed) quoted = true;
99
+ else if (char === ";" || char === "\n" || char === "\r") {
100
+ row.push(field); field = ""; closed = false;
101
+ if (char !== ";") { rows.push(row); row = []; if (char === "\r" && text[i + 1] === "\n") i++; }
102
+ } else {
103
+ if (closed || char === '"') throw new ConnectorError("connector_response_invalid", "Semrush returned malformed CSV.", { statusCode: 502 });
104
+ field += char;
105
+ }
106
+ }
107
+ if (quoted) throw new ConnectorError("connector_response_invalid", "Semrush returned incomplete CSV.", { statusCode: 502 });
108
+ if (field !== "" || closed || row.length) { row.push(field); rows.push(row); }
109
+ return rows;
110
+ }
111
+
112
+ function validProject(project) {
113
+ return Number.isSafeInteger(project?.project_id) && project.project_id > 0 &&
114
+ typeof project.project_name === "string" && project.project_name.length > 0 &&
115
+ typeof project.domain === "string" && project.domain.length > 0 &&
116
+ Number.isSafeInteger(project.owner_id) && project.owner_id > 0 &&
117
+ ["read", "edit", "delete", "share", "downgraded"].every((permission) => typeof project.permissions?.[permission] === "boolean") &&
118
+ Array.isArray(project.tools) && project.tools.every((tool) => typeof tool?.name === "string" && tool.name.length > 0);
119
+ }
120
+
121
+ const semrushProvider = Object.freeze({
122
+ ...semrushDefinition, apiOrigins: ["https://api.semrush.com"],
123
+ apiKey: { headers: (key) => {
124
+ if (/[\s\p{Cc}]/u.test(key)) {
125
+ throw new ConnectorError("connector_binding_missing", "Use the Semrush V4 API key without spaces or control characters.");
126
+ }
127
+ return { Authorization: `Apikey ${key}` };
128
+ } },
129
+ checkOperation: "projects.list",
130
+ operations: {
131
+ "tracking.campaigns": { scopes: [], request(input, settings) {
132
+ const { projectId } = validateSchemaPayload({ schema: projectSchema, mode: "replace" }, input, { statusCode: 422 });
133
+ if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the Semrush V3 key for campaign discovery.");
134
+ return { method: "GET", url: `https://api.semrush.com/management/v1/projects/${projectId}/tracking/campaigns` };
135
+ } },
136
+ "tracking.dates": semrushTrackingReport("tracking_campaign_dates"),
137
+ "tracking.organicPositions": semrushTrackingReport("tracking_position_organic", true),
138
+ "tracking.paidPositions": semrushTrackingReport("tracking_position_adwords", true),
139
+ "domains.organicKeywords": semrushKeywordReport("domain", "organic"),
140
+ "domains.paidKeywords": semrushKeywordReport("domain", "adwords"),
141
+ "urls.organicKeywords": semrushKeywordReport("url", "organic"),
142
+ "urls.paidKeywords": semrushKeywordReport("url", "adwords"),
143
+ "subfolders.organicKeywords": semrushKeywordReport("subfolder", "organic"),
144
+ "subfolders.paidKeywords": semrushKeywordReport("subfolder", "adwords"),
145
+ "domains.overview": {
146
+ scopes: [],
147
+ request(input, settings) {
148
+ if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the separate Semrush V3 API key reference for domain reports.");
149
+ const operation = jsonOperation("https://api.semrush.com/", {
150
+ domain: { type: "string", required: true, minLength: 1, maxLength: 253, pattern: /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/iu },
151
+ database: { type: "string", required: true, pattern: /^[a-z]{2}$/u }
152
+ });
153
+ const result = operation.request(input);
154
+ const url = new URL(result.url);
155
+ url.searchParams.set("type", "domain_rank");
156
+ url.searchParams.set("export_columns", "Dn,Rk,Or,Ot,Oc,Ad,At,Ac");
157
+ url.searchParams.set("export_escape", "1");
158
+ url.searchParams.set("export_decode", "1");
159
+ return { ...result, url: url.href };
160
+ }
161
+ },
162
+ "backlinks.overview": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/overview", backlinkTarget),
163
+ "backlinks.list": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/links", backlinkPage),
164
+ "backlinks.referringDomains": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/ref-domains", backlinkPage),
165
+ "backlinks.anchors": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/anchors", backlinkPage),
166
+ "keywords.metrics": {
167
+ scopes: [],
168
+ request(input) {
169
+ const operation = jsonOperation("https://api.semrush.com/apis/v4/keywords/v1/metrics", {
170
+ keyword: { type: "string", required: true, minLength: 1, maxLength: 255 },
171
+ country: { type: "string", required: true, pattern: /^[A-Z]{2}$/u },
172
+ month: { type: "string", pattern: /^\d{4}-(0[1-9]|1[0-2])$/u }
173
+ });
174
+ const result = operation.request(input);
175
+ const url = new URL(result.url);
176
+ const month = url.searchParams.get("month");
177
+ if (month && (month < "2012-01" || month > new Date().toISOString().slice(0, 7))) {
178
+ throw new ConnectorError("connector_input_invalid", "Choose a month from 2012-01 through the current month.", { statusCode: 422 });
179
+ }
180
+ url.searchParams.set("format", "json");
181
+ return { ...result, url: url.href };
182
+ }
183
+ },
184
+ "projects.create": jsonOperation(projectsUrl, {
185
+ domain: { type: "string", required: true, minLength: 1, maxLength: 253, pattern: /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/iu },
186
+ project_name: projectName
187
+ }, undefined, "POST"),
188
+ "projects.update": {
189
+ scopes: [],
190
+ request(input) {
191
+ const { projectId, project_name } = validateSchemaPayload({ schema: renameSchema, mode: "replace" }, input, { statusCode: 422 });
192
+ return { method: "PATCH", url: `${projectsUrl}/${projectId}`, body: { project_name } };
193
+ }
194
+ },
195
+ "projects.list": jsonOperation(projectsUrl, {
196
+ scope: { type: "string", enum: ["OWN", "ALL", "SHARED", "CORPORATE"], defaultTo: "OWN" },
197
+ limit: { type: "integer", min: 1, max: 1000, defaultTo: 100 },
198
+ offset: { type: "integer", min: 0, max: Number.MAX_SAFE_INTEGER, defaultTo: 0 }
199
+ }),
200
+ "projects.delete": {
201
+ scopes: [],
202
+ request(input) {
203
+ const { projectId } = validateSchemaPayload({ schema: projectSchema, mode: "replace" }, input, { statusCode: 422 });
204
+ return { method: "DELETE", url: `${projectsUrl}/${projectId}` };
205
+ }
206
+ },
207
+ "projects.get": {
208
+ scopes: [],
209
+ request(input) {
210
+ const { projectId } = validateSchemaPayload({ schema: projectSchema, mode: "replace" }, input, { statusCode: 422 });
211
+ return { method: "GET", url: `${projectsUrl}/${projectId}` };
212
+ }
213
+ }
214
+ },
215
+ async exchange(address, options, { request, fetchImpl, settings, resolveReference }) {
216
+ const path = new URL(address).pathname;
217
+ if (path === "/" || path.startsWith("/management/v1/projects/") || path.startsWith("/reports/v1/projects/")) {
218
+ let key;
219
+ try { key = await resolveReference(settings.v3ApiKeyRef); } catch {
220
+ throw new ConnectorError("connector_binding_missing", "Provide the Semrush V3 API key through its private Env reference.");
221
+ }
222
+ if (typeof key !== "string" || !key || key === "MISSING" || /[\s\p{Cc}]/u.test(key)) {
223
+ throw new ConnectorError("connector_binding_missing", "Provide a valid Semrush V3 API key.");
224
+ }
225
+ const url = new URL(address);
226
+ url.searchParams.set("key", key);
227
+ let response, text;
228
+ try {
229
+ response = await fetchImpl(url.href, { method: "GET", headers: { Accept: "text/csv" },
230
+ credentials: "omit", redirect: "error", signal: options.signal });
231
+ if (!response.ok) throw new Error("Provider failure");
232
+ text = await response.text();
233
+ } catch {
234
+ options.signal.throwIfAborted();
235
+ const status = response?.status;
236
+ throw new ConnectorError(status === 429 ? "connector_rate_limited" : status === 403 ? "connector_permission_denied" : "connector_provider_failed",
237
+ "Semrush could not return the V3 report.", { statusCode: status === 429 ? 429 : status === 403 ? 403 : 502 });
238
+ }
239
+ if (text.length > 2_000_000) throw new ConnectorError("connector_response_invalid", "Semrush returned an oversized V3 report.", { statusCode: 502 });
240
+ if (path !== "/") {
241
+ let result;
242
+ try { result = JSON.parse(text); } catch {
243
+ throw new ConnectorError("connector_response_invalid", "Semrush returned invalid tracking JSON.", { statusCode: 502 });
244
+ }
245
+ let valid = result && typeof result === "object" && !Array.isArray(result) && result.error === undefined;
246
+ if (path.endsWith("/campaigns")) {
247
+ valid &&= String(result.project_id) === path.split("/")[4] && Array.isArray(result.campaigns) &&
248
+ result.campaigns.every((campaign) => typeof campaign?.id === "string" && /^\d+(?:_\d+)?$/u.test(campaign.id) &&
249
+ typeof campaign.url === "string" && typeof campaign.device === "string" && typeof campaign.isGathering === "boolean");
250
+ } else {
251
+ valid &&= result.data !== null && typeof result.data === "object" &&
252
+ /^(0|[1-9]\d*)$/u.test(String(result.total));
253
+ if (url.searchParams.get("type") === "tracking_campaign_dates") {
254
+ valid &&= Object.values(result.data).every((entry) => /^\d{8}$/u.test(String(entry?.Dt)));
255
+ } else {
256
+ valid &&= String(result.limit) === url.searchParams.get("display_limit") && String(result.offset) === url.searchParams.get("display_offset") &&
257
+ Object.values(result.data).length <= Number(url.searchParams.get("display_limit")) &&
258
+ Object.values(result.data).every((entry) => typeof entry?.Ph === "string" && typeof entry.Pi === "string" && entry.Dt !== null && typeof entry.Dt === "object");
259
+ }
260
+ }
261
+ if (!valid) throw new ConnectorError("connector_response_invalid", "Semrush returned unexpected tracking data.", { statusCode: 502 });
262
+ return result;
263
+ }
264
+ const columns = url.searchParams.get("type") === "domain_rank" ? domainColumns : keywordColumns;
265
+ const rowLimit = columns === domainColumns ? 1 : Number(url.searchParams.get("display_limit")) - Number(url.searchParams.get("display_offset"));
266
+ const error = /^ERROR\s+(\d+)/u.exec(text.trim());
267
+ if (error) {
268
+ if (error[1] === "50") return { columns, rows: [] };
269
+ const code = Number(error[1]);
270
+ if ([110, 120].includes(code)) throw new ConnectorError("connector_binding_missing", "Replace the separate Semrush V3 key in Env.");
271
+ if ([130, 133, 135].includes(code)) throw new ConnectorError("connector_permission_denied", "Check Semrush V3 API, database and report entitlement.", { statusCode: 403 });
272
+ if ([131, 132, 134].includes(code)) throw new ConnectorError("connector_quota_limited", "Check Semrush V3 API units and report limits.", { statusCode: 429 });
273
+ throw new ConnectorError("connector_provider_failed", "Semrush rejected the V3 report. Check its inputs and account access.", { statusCode: 502 });
274
+ }
275
+ const rows = parseSemrushCsv(text);
276
+ if (JSON.stringify(rows.shift()) !== JSON.stringify(columns) || rows.length > rowLimit ||
277
+ rows.some((row) => row.length !== columns.length)) {
278
+ throw new ConnectorError("connector_response_invalid", "Semrush returned unexpected V3 report columns or rows.", { statusCode: 502 });
279
+ }
280
+ return { columns, rows };
281
+ }
282
+ const result = await request(address, options);
283
+ if (result?.meta?.success === false) {
284
+ const code = result.error?.code;
285
+ if ([70, 120, 121, 122].includes(code)) {
286
+ throw new ConnectorError("connector_reconnect_required", "Replace the Semrush V4 key and connect again.", { statusCode: 401 });
287
+ }
288
+ if (code === 130) {
289
+ throw new ConnectorError("connector_permission_denied", "Semrush API access is disabled for this account.", { statusCode: 403 });
290
+ }
291
+ if ([131, 132, 134].includes(code)) {
292
+ throw new ConnectorError("connector_quota_limited", "Semrush API capacity is exhausted. Check the account's subscription and available units.", { statusCode: 429 });
293
+ }
294
+ if (code === 512) {
295
+ throw new ConnectorError("connector_resource_not_found", "The Semrush project was not found or is not accessible.", { statusCode: 404 });
296
+ }
297
+ throw new ConnectorError("connector_provider_failed", "Semrush could not complete this request.", { statusCode: 502 });
298
+ }
299
+ const url = new URL(address);
300
+ const meta = result?.meta;
301
+ const creating = options.method === "POST" && url.pathname === "/apis/v4/projects/v1/projects";
302
+ let valid = meta?.success === true && meta.status_code === (creating ? 201 : 200) &&
303
+ typeof meta.request_id === "string" && meta.request_id.length > 0 && result.error === undefined;
304
+ if (url.pathname === "/apis/v4/keywords/v1/metrics") {
305
+ const data = result?.data;
306
+ valid &&= meta.keyword === url.searchParams.get("keyword") && meta.country === url.searchParams.get("country") &&
307
+ typeof meta.month === "string" && /^\d{4}-(0[1-9]|1[0-2])$/u.test(meta.month) &&
308
+ (!url.searchParams.has("month") || meta.month === url.searchParams.get("month")) &&
309
+ data !== null && typeof data === "object" && !Array.isArray(data) &&
310
+ ["cpc", "number_of_results", "search_volume"].every((key) => typeof data[key] === "string" && /^\d+$/u.test(data[key])) &&
311
+ ["competitive_density", "keyword_difficulty"].every((key) => Number.isFinite(data[key])) &&
312
+ ["intents", "serp_features"].every((key) => Array.isArray(data[key]) && data[key].every((value) => typeof value === "string")) &&
313
+ Array.isArray(data.trends) && data.trends.every(Number.isFinite);
314
+ } else if (url.pathname.startsWith("/apis/v4/backlinks/v1/")) {
315
+ const report = url.pathname.split("/").at(-1);
316
+ const count = (value) => Number.isSafeInteger(value) && value >= 0;
317
+ if (report === "overview") {
318
+ valid &&= ["backlinks_count", "domains_count", "urls_count", "score"].every((field) => count(result.data?.[field]));
319
+ } else {
320
+ valid &&= Array.isArray(result.data) && result.data.length <= Number(url.searchParams.get("limit")) &&
321
+ result.data.every((row) => {
322
+ if (!row || typeof row !== "object" || Array.isArray(row)) return false;
323
+ if (report === "links") return typeof row.source_url === "string" && typeof row.target_url === "string" &&
324
+ typeof row.anchor === "string" && typeof row.is_nofollow === "boolean";
325
+ if (report === "ref-domains") return typeof row.domain === "string" && count(row.backlinks_count) && count(row.domain_score);
326
+ return typeof row.anchor === "string" && count(row.backlinks_count) && count(row.domains_count);
327
+ });
328
+ // Some report envelopes omit pagination metadata; validate it when supplied.
329
+ for (const field of ["limit", "offset"]) {
330
+ if (meta?.[field] !== undefined) valid &&= String(meta[field]) === url.searchParams.get(field);
331
+ }
332
+ if (meta?.total !== undefined) valid &&= count(meta.total);
333
+ }
334
+ if (meta?.scope !== undefined) valid &&= meta.scope === url.searchParams.get("scope");
335
+ } else if (creating) {
336
+ const submitted = options.body;
337
+ valid &&= validProject(result.data) && result.data.project_name === submitted.project_name &&
338
+ result.data.domain.toLowerCase() === submitted.domain.toLowerCase();
339
+ } else if (url.pathname === "/apis/v4/projects/v1/projects") {
340
+ const limit = Number(url.searchParams.get("limit"));
341
+ valid &&= meta.scope === url.searchParams.get("scope") && meta.limit === limit &&
342
+ meta.offset === Number(url.searchParams.get("offset")) &&
343
+ Number.isSafeInteger(meta.total_count) && meta.total_count >= 0 &&
344
+ Array.isArray(result.data) && result.data.length <= limit && result.data.length <= meta.total_count &&
345
+ result.data.every(validProject);
346
+ } else if (options.method === "DELETE") {
347
+ valid &&= Number.isSafeInteger(result.data?.project_id) &&
348
+ result.data.project_id === Number(url.pathname.split("/").at(-1));
349
+ } else {
350
+ valid &&= validProject(result.data) && result.data.project_id === Number(url.pathname.split("/").at(-1));
351
+ if (options.method === "PATCH") valid &&= result.data.project_name === options.body.project_name;
352
+ }
353
+ if (!valid) {
354
+ throw new ConnectorError("connector_response_invalid", "Semrush returned unexpected report or project data.", { statusCode: 502 });
355
+ }
356
+ return result;
357
+ }
358
+ });
359
+ export { semrushProvider };
@@ -0,0 +1,30 @@
1
+ import { sentryDefinition } from "../shared/tokens.js";
2
+ import { createSchema } from "json-rest-schema";
3
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
4
+ import { mcpOperations, exchangeMcp } from "./mcpTransport.js";
5
+ import { mcpRegistrationFields, registerMcpClient } from "./mcpRegistration.js";
6
+
7
+ const oauth = Object.freeze({
8
+ issuer: "https://mcp.sentry.dev", authorization_endpoint: "https://mcp.sentry.dev/oauth/authorize",
9
+ token_endpoint: "https://mcp.sentry.dev/oauth/token", registration_endpoint: "https://mcp.sentry.dev/oauth/register",
10
+ response_types_supported: ["code"], code_challenge_methods_supported: ["S256"]
11
+ });
12
+ const registrationSchema = createSchema({ ...mcpRegistrationFields,
13
+ scopes: { type: "array", required: true, minLength: 1,
14
+ items: { type: "string", enum: sentryDefinition.scopes.map((scope) => scope.value) } }
15
+ });
16
+ async function registerSentryClient(input, options) {
17
+ const values = validateSchemaPayload({ schema: registrationSchema, mode: "replace" }, input, { statusCode: 422 });
18
+ return registerMcpClient("Sentry", oauth, values, options);
19
+ }
20
+ function sentryEndpoint(settings) {
21
+ const values = validateSchemaPayload({ schema: sentryDefinition.settingsSchema, mode: "replace" }, settings, { statusCode: 422 });
22
+ return `https://mcp.sentry.dev/mcp/${values.organizationSlug}${values.projectSlug ? `/${values.projectSlug}` : ""}`;
23
+ }
24
+
25
+ const sentryProvider = Object.freeze({
26
+ ...sentryDefinition, oauth, oauthResource: sentryEndpoint,
27
+ apiOrigins: ["https://mcp.sentry.dev"], checkOperation: "tools.list",
28
+ exchange: exchangeMcp, operations: mcpOperations(sentryEndpoint)
29
+ });
30
+ export { sentryProvider, registerSentryClient };
@@ -0,0 +1,20 @@
1
+ import { sevdeskDefinition } from "../shared/tokens.js";
2
+ import { jsonOperation } from "./jsonOperation.js";
3
+
4
+ const sevdeskProvider = Object.freeze({
5
+ ...sevdeskDefinition,
6
+ apiOrigins: ["https://my.sevdesk.de"],
7
+ apiKey: { headers: (key) => ({ Authorization: key }) },
8
+ checkOperation: "contacts.list",
9
+ operations: {
10
+ "contacts.list": jsonOperation("https://my.sevdesk.de/api/v1/Contact", {
11
+ limit: { type: "integer", min: 1, max: 1000, defaultTo: 100 },
12
+ offset: { type: "integer", min: 0, max: 1000000, defaultTo: 0 },
13
+ countAll: { type: "boolean", defaultTo: false },
14
+ depth: { type: "integer", enum: [0, 1], defaultTo: 1 }
15
+ }, (result) => Array.isArray(result?.objects) &&
16
+ (result.total === undefined || Number.isInteger(result.total) && result.total >= 0 ||
17
+ typeof result.total === "string" && /^[0-9]+$/u.test(result.total)))
18
+ }
19
+ });
20
+ export { sevdeskProvider };
@@ -0,0 +1,99 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
3
+ import { shopifyDefinition } from "../shared/shopify.js";
4
+ import { graphqlOperation } from "./graphqlOperation.js";
5
+
6
+ const origin = (settings) => `https://${settings.shopDomain}`;
7
+ const endpoint = (settings) => `${origin(settings)}/admin/api/2026-07/graphql.json`;
8
+ const productId = (value) => typeof value === "string" && /^gid:\/\/shopify\/Product\/[1-9][0-9]*$/u.test(value);
9
+ const idField = { type: "string", required: true, noTrim: true, maxLength: 100, validator: (value) => productId(value) || "Use a Shopify Product GID." };
10
+ const productFields = {
11
+ title: { type: "string", minLength: 1, maxLength: 255, noTrim: true },
12
+ descriptionHtml: { type: "string", maxLength: 65536, noTrim: true },
13
+ vendor: { type: "string", maxLength: 255, noTrim: true },
14
+ productType: { type: "string", maxLength: 255, noTrim: true },
15
+ status: { type: "string", enum: ["ACTIVE", "ARCHIVED", "DRAFT"] },
16
+ tags: { type: "array", validator: (value) => value.length <= 250 || "Use at most 250 tags.",
17
+ items: { type: "string", minLength: 1, maxLength: 255, noTrim: true } }
18
+ };
19
+ const productSelection = "id title handle status";
20
+ const validProduct = (product) => productId(product?.id) && typeof product.title === "string" && typeof product.handle === "string" &&
21
+ ["ACTIVE", "ARCHIVED", "DRAFT"].includes(product.status);
22
+ const emptyUserErrors = (payload) => Array.isArray(payload?.userErrors) && payload.userErrors.length === 0;
23
+
24
+ function productMutation(name, fields) {
25
+ const operation = graphqlOperation(endpoint,
26
+ `mutation ConnectorProduct${name}($product: Product${name}Input!) { product${name}(product: $product) { product { ${productSelection} } userErrors { field message } } }`,
27
+ { product: { type: "object", required: true, schema: createSchema(fields) } },
28
+ (data) => emptyUserErrors(data?.[`product${name}`]) && validProduct(data[`product${name}`].product));
29
+ return { ...operation, scopes: ["write_products"], request(input, settings) {
30
+ const request = operation.request(input, settings);
31
+ if (name === "Update" && Object.keys(request.body.variables.product).length === 1) {
32
+ throw new ConnectorError("connector_input_invalid", "Select at least one product detail to update.", { statusCode: 422 });
33
+ }
34
+ return request;
35
+ } };
36
+ }
37
+
38
+ const shopifyProvider = Object.freeze({
39
+ ...shopifyDefinition,
40
+ oauth: (settings) => ({ issuer: origin(settings), token_endpoint: `${origin(settings)}/admin/oauth/access_token` }),
41
+ scopeSeparator: ",", apiOrigins: (settings) => [origin(settings)],
42
+ apiKey: { headers: (token) => ({ "X-Shopify-Access-Token": token }) },
43
+ checkOperation: "products.list",
44
+ async normalizeTokenResponse(response) {
45
+ if (!response.ok) return response;
46
+ let token;
47
+ try { token = await response.clone().json(); } catch { /* reported below */ }
48
+ if (typeof token?.access_token !== "string" || !token.access_token.trim() ||
49
+ !Number.isSafeInteger(token.expires_in) || token.expires_in <= 0 ||
50
+ typeof token.scope !== "string" || !/^[a-z_]+(?:,[a-z_]+)*$/u.test(token.scope)) {
51
+ throw new ConnectorError("connector_response_invalid", "Shopify returned an invalid token grant.", { statusCode: 502 });
52
+ }
53
+ // Shopify's documented client-credentials response omits the standard token_type.
54
+ return Response.json({ ...token, token_type: token.token_type ?? "Bearer" });
55
+ },
56
+ operations: {
57
+ "products.list": { ...graphqlOperation(endpoint,
58
+ `query ConnectorProducts($first: Int!, $after: String, $query: String) { products(first: $first, after: $after, query: $query) { nodes { ${productSelection} } pageInfo { hasNextPage endCursor } } }`,
59
+ { first: { type: "integer", min: 1, max: 100, defaultTo: 20 },
60
+ after: { type: "string", minLength: 1, maxLength: 4096, noTrim: true },
61
+ query: { type: "string", minLength: 1, maxLength: 1024, noTrim: true } },
62
+ (data) => Array.isArray(data?.products?.nodes) && data.products.nodes.length <= 100 && data.products.nodes.every(validProduct) &&
63
+ typeof data.products.pageInfo?.hasNextPage === "boolean" &&
64
+ (data.products.pageInfo.endCursor === null || (typeof data.products.pageInfo.endCursor === "string" && data.products.pageInfo.endCursor.length > 0)) &&
65
+ (!data.products.pageInfo.hasNextPage || typeof data.products.pageInfo.endCursor === "string")),
66
+ scopes: ["read_products", "write_products"] },
67
+ "products.create": productMutation("Create", { ...productFields, title: { ...productFields.title, required: true }, status: { ...productFields.status, defaultTo: "DRAFT" } }),
68
+ "products.update": productMutation("Update", { id: idField, ...productFields }),
69
+ "products.delete": { ...graphqlOperation(endpoint,
70
+ "mutation ConnectorProductDelete($input: ProductDeleteInput!) { productDelete(input: $input, synchronous: true) { deletedProductId userErrors { field message } } }",
71
+ { input: { type: "object", required: true, schema: createSchema({ id: idField }) } },
72
+ (data) => emptyUserErrors(data?.productDelete) && productId(data.productDelete.deletedProductId)), scopes: ["write_products"] }
73
+ },
74
+ async exchange(address, options, { request }) {
75
+ const headers = { ...options.headers };
76
+ if (headers.Authorization) {
77
+ headers["X-Shopify-Access-Token"] = headers.Authorization.slice("Bearer ".length);
78
+ delete headers.Authorization;
79
+ }
80
+ const result = await request(address, { ...options, headers });
81
+ if (Array.isArray(result?.errors) && result.errors.length) {
82
+ const codes = result.errors.map((error) => error?.extensions?.code);
83
+ if (codes.includes("THROTTLED")) throw new ConnectorError("connector_rate_limited", "Shopify's request limit was reached.", { statusCode: 429 });
84
+ if (codes.includes("ACCESS_DENIED")) throw new ConnectorError("connector_permission_denied", "Shopify denied this operation.", { statusCode: 403 });
85
+ }
86
+ for (const name of ["productCreate", "productUpdate", "productDelete"]) {
87
+ const payload = result?.data?.[name];
88
+ if (Array.isArray(payload?.userErrors) && payload.userErrors.length) {
89
+ throw new ConnectorError("connector_operation_rejected", "Shopify rejected the product change. Check the product values and permissions before retrying.", { statusCode: 422 });
90
+ }
91
+ }
92
+ if (result?.data?.products?.nodes?.length > options.body.variables.first) {
93
+ throw new ConnectorError("connector_response_invalid", "Shopify returned more products than requested.", { statusCode: 502 });
94
+ }
95
+ return result;
96
+ }
97
+ });
98
+
99
+ export { shopifyProvider };