@memberjunction/server 3.3.0 → 4.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 (296) hide show
  1. package/README.md +59 -0
  2. package/dist/agents/skip-agent.d.ts +65 -0
  3. package/dist/agents/skip-agent.d.ts.map +1 -1
  4. package/dist/agents/skip-agent.js +63 -5
  5. package/dist/agents/skip-agent.js.map +1 -1
  6. package/dist/agents/skip-sdk.d.ts +163 -0
  7. package/dist/agents/skip-sdk.d.ts.map +1 -1
  8. package/dist/agents/skip-sdk.js +143 -12
  9. package/dist/agents/skip-sdk.js.map +1 -1
  10. package/dist/apolloServer/TransactionPlugin.d.ts +4 -0
  11. package/dist/apolloServer/TransactionPlugin.d.ts.map +1 -0
  12. package/dist/apolloServer/TransactionPlugin.js +46 -0
  13. package/dist/apolloServer/TransactionPlugin.js.map +1 -0
  14. package/dist/apolloServer/index.d.ts +0 -1
  15. package/dist/apolloServer/index.d.ts.map +1 -1
  16. package/dist/auth/APIKeyScopeAuth.d.ts +82 -0
  17. package/dist/auth/APIKeyScopeAuth.d.ts.map +1 -1
  18. package/dist/auth/APIKeyScopeAuth.js +78 -0
  19. package/dist/auth/APIKeyScopeAuth.js.map +1 -1
  20. package/dist/auth/AuthProviderFactory.d.ts +35 -0
  21. package/dist/auth/AuthProviderFactory.d.ts.map +1 -1
  22. package/dist/auth/AuthProviderFactory.js +51 -4
  23. package/dist/auth/AuthProviderFactory.js.map +1 -1
  24. package/dist/auth/BaseAuthProvider.d.ts +22 -0
  25. package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
  26. package/dist/auth/BaseAuthProvider.js +25 -8
  27. package/dist/auth/BaseAuthProvider.js.map +1 -1
  28. package/dist/auth/IAuthProvider.d.ts +33 -0
  29. package/dist/auth/IAuthProvider.d.ts.map +1 -1
  30. package/dist/auth/__tests__/backward-compatibility.test.d.ts +2 -0
  31. package/dist/auth/__tests__/backward-compatibility.test.d.ts.map +1 -0
  32. package/dist/auth/__tests__/backward-compatibility.test.js +135 -0
  33. package/dist/auth/__tests__/backward-compatibility.test.js.map +1 -0
  34. package/dist/auth/exampleNewUserSubClass.d.ts +5 -1
  35. package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
  36. package/dist/auth/exampleNewUserSubClass.js +21 -6
  37. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  38. package/dist/auth/index.d.ts +14 -0
  39. package/dist/auth/index.d.ts.map +1 -1
  40. package/dist/auth/index.js +35 -22
  41. package/dist/auth/index.js.map +1 -1
  42. package/dist/auth/initializeProviders.d.ts +3 -0
  43. package/dist/auth/initializeProviders.d.ts.map +1 -1
  44. package/dist/auth/initializeProviders.js +6 -0
  45. package/dist/auth/initializeProviders.js.map +1 -1
  46. package/dist/auth/newUsers.js +11 -2
  47. package/dist/auth/newUsers.js.map +1 -1
  48. package/dist/auth/providers/Auth0Provider.d.ts +9 -0
  49. package/dist/auth/providers/Auth0Provider.d.ts.map +1 -1
  50. package/dist/auth/providers/Auth0Provider.js +10 -0
  51. package/dist/auth/providers/Auth0Provider.js.map +1 -1
  52. package/dist/auth/providers/CognitoProvider.d.ts +9 -0
  53. package/dist/auth/providers/CognitoProvider.d.ts.map +1 -1
  54. package/dist/auth/providers/CognitoProvider.js +10 -0
  55. package/dist/auth/providers/CognitoProvider.js.map +1 -1
  56. package/dist/auth/providers/GoogleProvider.d.ts +9 -0
  57. package/dist/auth/providers/GoogleProvider.d.ts.map +1 -1
  58. package/dist/auth/providers/GoogleProvider.js +11 -1
  59. package/dist/auth/providers/GoogleProvider.js.map +1 -1
  60. package/dist/auth/providers/MSALProvider.d.ts +9 -0
  61. package/dist/auth/providers/MSALProvider.d.ts.map +1 -1
  62. package/dist/auth/providers/MSALProvider.js +10 -0
  63. package/dist/auth/providers/MSALProvider.js.map +1 -1
  64. package/dist/auth/providers/OktaProvider.d.ts +9 -0
  65. package/dist/auth/providers/OktaProvider.d.ts.map +1 -1
  66. package/dist/auth/providers/OktaProvider.js +10 -0
  67. package/dist/auth/providers/OktaProvider.js.map +1 -1
  68. package/dist/config.d.ts +12 -0
  69. package/dist/config.d.ts.map +1 -1
  70. package/dist/config.js +44 -10
  71. package/dist/config.js.map +1 -1
  72. package/dist/context.d.ts +8 -1
  73. package/dist/context.d.ts.map +1 -1
  74. package/dist/context.js +26 -4
  75. package/dist/context.js.map +1 -1
  76. package/dist/directives/Public.js +2 -0
  77. package/dist/directives/Public.js.map +1 -1
  78. package/dist/entitySubclasses/entityPermissions.server.d.ts +7 -2
  79. package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
  80. package/dist/entitySubclasses/entityPermissions.server.js +26 -8
  81. package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
  82. package/dist/generated/generated.d.ts +954 -2
  83. package/dist/generated/generated.d.ts.map +1 -1
  84. package/dist/generated/generated.js +12183 -14532
  85. package/dist/generated/generated.js.map +1 -1
  86. package/dist/generic/DeleteOptionsInput.d.ts +3 -0
  87. package/dist/generic/DeleteOptionsInput.d.ts.map +1 -1
  88. package/dist/generic/DeleteOptionsInput.js +3 -2
  89. package/dist/generic/DeleteOptionsInput.js.map +1 -1
  90. package/dist/generic/KeyInputOutputTypes.js +0 -6
  91. package/dist/generic/KeyInputOutputTypes.js.map +1 -1
  92. package/dist/generic/KeyValuePairInput.d.ts +4 -0
  93. package/dist/generic/KeyValuePairInput.d.ts.map +1 -1
  94. package/dist/generic/KeyValuePairInput.js +4 -2
  95. package/dist/generic/KeyValuePairInput.js.map +1 -1
  96. package/dist/generic/PushStatusResolver.js +0 -3
  97. package/dist/generic/PushStatusResolver.js.map +1 -1
  98. package/dist/generic/ResolverBase.d.ts +59 -0
  99. package/dist/generic/ResolverBase.d.ts.map +1 -1
  100. package/dist/generic/ResolverBase.js +233 -18
  101. package/dist/generic/ResolverBase.js.map +1 -1
  102. package/dist/generic/RunViewResolver.d.ts +22 -0
  103. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  104. package/dist/generic/RunViewResolver.js +42 -108
  105. package/dist/generic/RunViewResolver.js.map +1 -1
  106. package/dist/index.d.ts +2 -2
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +84 -39
  109. package/dist/index.js.map +1 -1
  110. package/dist/orm.d.ts.map +1 -1
  111. package/dist/orm.js +2 -1
  112. package/dist/orm.js.map +1 -1
  113. package/dist/resolvers/APIKeyResolver.d.ts +76 -1
  114. package/dist/resolvers/APIKeyResolver.d.ts.map +1 -1
  115. package/dist/resolvers/APIKeyResolver.js +53 -11
  116. package/dist/resolvers/APIKeyResolver.js.map +1 -1
  117. package/dist/resolvers/ActionResolver.d.ts +191 -1
  118. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  119. package/dist/resolvers/ActionResolver.js +156 -22
  120. package/dist/resolvers/ActionResolver.js.map +1 -1
  121. package/dist/resolvers/ColorResolver.js +0 -5
  122. package/dist/resolvers/ColorResolver.js.map +1 -1
  123. package/dist/resolvers/ComponentRegistryResolver.d.ts +65 -0
  124. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  125. package/dist/resolvers/ComponentRegistryResolver.js +118 -40
  126. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  127. package/dist/resolvers/CreateQueryResolver.d.ts +47 -0
  128. package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
  129. package/dist/resolvers/CreateQueryResolver.js +92 -116
  130. package/dist/resolvers/CreateQueryResolver.js.map +1 -1
  131. package/dist/resolvers/DatasetResolver.d.ts +5 -4
  132. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  133. package/dist/resolvers/DatasetResolver.js +9 -18
  134. package/dist/resolvers/DatasetResolver.js.map +1 -1
  135. package/dist/resolvers/EntityCommunicationsResolver.d.ts +42 -1
  136. package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
  137. package/dist/resolvers/EntityCommunicationsResolver.js +5 -37
  138. package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
  139. package/dist/resolvers/EntityRecordNameResolver.js +0 -7
  140. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  141. package/dist/resolvers/FileCategoryResolver.js +13 -1
  142. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  143. package/dist/resolvers/FileResolver.d.ts +16 -0
  144. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  145. package/dist/resolvers/FileResolver.js +59 -74
  146. package/dist/resolvers/FileResolver.js.map +1 -1
  147. package/dist/resolvers/GetDataContextDataResolver.d.ts +20 -2
  148. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  149. package/dist/resolvers/GetDataContextDataResolver.js +27 -12
  150. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  151. package/dist/resolvers/GetDataResolver.d.ts +19 -0
  152. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  153. package/dist/resolvers/GetDataResolver.js +35 -35
  154. package/dist/resolvers/GetDataResolver.js.map +1 -1
  155. package/dist/resolvers/InfoResolver.d.ts.map +1 -1
  156. package/dist/resolvers/InfoResolver.js +4 -7
  157. package/dist/resolvers/InfoResolver.js.map +1 -1
  158. package/dist/resolvers/MCPResolver.d.ts +361 -0
  159. package/dist/resolvers/MCPResolver.d.ts.map +1 -0
  160. package/dist/resolvers/MCPResolver.js +1270 -0
  161. package/dist/resolvers/MCPResolver.js.map +1 -0
  162. package/dist/resolvers/MergeRecordsResolver.d.ts +2 -1
  163. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  164. package/dist/resolvers/MergeRecordsResolver.js +6 -30
  165. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  166. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
  167. package/dist/resolvers/PotentialDuplicateRecordResolver.js +0 -3
  168. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  169. package/dist/resolvers/QueryResolver.d.ts +22 -1
  170. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  171. package/dist/resolvers/QueryResolver.js +50 -37
  172. package/dist/resolvers/QueryResolver.js.map +1 -1
  173. package/dist/resolvers/ReportResolver.d.ts +5 -1
  174. package/dist/resolvers/ReportResolver.d.ts.map +1 -1
  175. package/dist/resolvers/ReportResolver.js +13 -11
  176. package/dist/resolvers/ReportResolver.js.map +1 -1
  177. package/dist/resolvers/RunAIAgentResolver.d.ts +54 -0
  178. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  179. package/dist/resolvers/RunAIAgentResolver.js +118 -40
  180. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  181. package/dist/resolvers/RunAIPromptResolver.d.ts +42 -0
  182. package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
  183. package/dist/resolvers/RunAIPromptResolver.js +98 -22
  184. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  185. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  186. package/dist/resolvers/RunTemplateResolver.js +10 -6
  187. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  188. package/dist/resolvers/RunTestResolver.d.ts +12 -0
  189. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  190. package/dist/resolvers/RunTestResolver.js +35 -21
  191. package/dist/resolvers/RunTestResolver.js.map +1 -1
  192. package/dist/resolvers/SqlLoggingConfigResolver.d.ts +312 -0
  193. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  194. package/dist/resolvers/SqlLoggingConfigResolver.js +295 -45
  195. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  196. package/dist/resolvers/SyncDataResolver.d.ts +21 -0
  197. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  198. package/dist/resolvers/SyncDataResolver.js +36 -22
  199. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  200. package/dist/resolvers/SyncRolesUsersResolver.d.ts +14 -0
  201. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  202. package/dist/resolvers/SyncRolesUsersResolver.js +54 -21
  203. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  204. package/dist/resolvers/TaskResolver.d.ts +13 -0
  205. package/dist/resolvers/TaskResolver.d.ts.map +1 -1
  206. package/dist/resolvers/TaskResolver.js +23 -7
  207. package/dist/resolvers/TaskResolver.js.map +1 -1
  208. package/dist/resolvers/TelemetryResolver.d.ts +22 -0
  209. package/dist/resolvers/TelemetryResolver.d.ts.map +1 -1
  210. package/dist/resolvers/TelemetryResolver.js +45 -79
  211. package/dist/resolvers/TelemetryResolver.js.map +1 -1
  212. package/dist/resolvers/TransactionGroupResolver.js +11 -13
  213. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  214. package/dist/resolvers/UserFavoriteResolver.js +3 -12
  215. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  216. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  217. package/dist/resolvers/UserResolver.js +14 -0
  218. package/dist/resolvers/UserResolver.js.map +1 -1
  219. package/dist/resolvers/UserViewResolver.js +4 -0
  220. package/dist/resolvers/UserViewResolver.js.map +1 -1
  221. package/dist/resolvers/VersionHistoryResolver.d.ts +39 -0
  222. package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -0
  223. package/dist/resolvers/VersionHistoryResolver.js +208 -0
  224. package/dist/resolvers/VersionHistoryResolver.js.map +1 -0
  225. package/dist/rest/EntityCRUDHandler.d.ts +19 -0
  226. package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
  227. package/dist/rest/EntityCRUDHandler.js +55 -0
  228. package/dist/rest/EntityCRUDHandler.js.map +1 -1
  229. package/dist/rest/OAuthCallbackHandler.d.ts +143 -0
  230. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -0
  231. package/dist/rest/OAuthCallbackHandler.js +634 -0
  232. package/dist/rest/OAuthCallbackHandler.js.map +1 -0
  233. package/dist/rest/RESTEndpointHandler.d.ts +120 -0
  234. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  235. package/dist/rest/RESTEndpointHandler.js +213 -24
  236. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  237. package/dist/rest/ViewOperationsHandler.d.ts +19 -0
  238. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  239. package/dist/rest/ViewOperationsHandler.js +39 -0
  240. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  241. package/dist/rest/index.d.ts +1 -0
  242. package/dist/rest/index.d.ts.map +1 -1
  243. package/dist/rest/index.js +1 -0
  244. package/dist/rest/index.js.map +1 -1
  245. package/dist/rest/setupRESTEndpoints.d.ts +35 -0
  246. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  247. package/dist/rest/setupRESTEndpoints.js +15 -1
  248. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  249. package/dist/services/ScheduledJobsService.d.ts +31 -0
  250. package/dist/services/ScheduledJobsService.d.ts.map +1 -1
  251. package/dist/services/ScheduledJobsService.js +38 -4
  252. package/dist/services/ScheduledJobsService.js.map +1 -1
  253. package/dist/services/TaskOrchestrator.d.ts +73 -0
  254. package/dist/services/TaskOrchestrator.d.ts.map +1 -1
  255. package/dist/services/TaskOrchestrator.js +137 -15
  256. package/dist/services/TaskOrchestrator.js.map +1 -1
  257. package/dist/types.d.ts +14 -0
  258. package/dist/types.d.ts.map +1 -1
  259. package/dist/types.js +0 -13
  260. package/dist/types.js.map +1 -1
  261. package/dist/util.d.ts +37 -1
  262. package/dist/util.d.ts.map +1 -1
  263. package/dist/util.js +55 -8
  264. package/dist/util.js.map +1 -1
  265. package/package.json +79 -77
  266. package/src/auth/BaseAuthProvider.ts +3 -0
  267. package/src/auth/IAuthProvider.ts +5 -0
  268. package/src/auth/exampleNewUserSubClass.ts +1 -5
  269. package/src/config.ts +2 -2
  270. package/src/entitySubclasses/entityPermissions.server.ts +1 -3
  271. package/src/generated/generated.ts +6245 -2558
  272. package/src/generic/ResolverBase.ts +89 -3
  273. package/src/index.ts +71 -64
  274. package/src/resolvers/APIKeyResolver.ts +8 -1
  275. package/src/resolvers/ActionResolver.ts +8 -1
  276. package/src/resolvers/DatasetResolver.ts +11 -4
  277. package/src/resolvers/EntityCommunicationsResolver.ts +5 -1
  278. package/src/resolvers/GetDataContextDataResolver.ts +14 -6
  279. package/src/resolvers/InfoResolver.ts +5 -1
  280. package/src/resolvers/MCPResolver.ts +1380 -0
  281. package/src/resolvers/MergeRecordsResolver.ts +5 -1
  282. package/src/resolvers/PotentialDuplicateRecordResolver.ts +0 -4
  283. package/src/resolvers/QueryResolver.ts +17 -3
  284. package/src/resolvers/ReportResolver.ts +8 -1
  285. package/src/resolvers/RunAIAgentResolver.ts +6 -0
  286. package/src/resolvers/RunAIPromptResolver.ts +10 -1
  287. package/src/resolvers/RunTemplateResolver.ts +4 -1
  288. package/src/resolvers/TaskResolver.ts +3 -0
  289. package/src/resolvers/UserResolver.ts +15 -3
  290. package/src/resolvers/VersionHistoryResolver.ts +177 -0
  291. package/src/rest/OAuthCallbackHandler.ts +766 -0
  292. package/src/rest/RESTEndpointHandler.ts +58 -35
  293. package/src/rest/index.ts +2 -1
  294. package/src/rest/setupRESTEndpoints.ts +13 -12
  295. package/src/resolvers/AskSkipResolver.ts +0 -3446
  296. package/src/scheduler/LearningCycleScheduler.ts +0 -320
@@ -0,0 +1,1380 @@
1
+ /**
2
+ * @fileoverview MCP GraphQL Resolver
3
+ *
4
+ * Provides GraphQL mutations for MCP (Model Context Protocol) operations
5
+ * including tool synchronization with progress streaming and OAuth management.
6
+ */
7
+
8
+ import { Resolver, Mutation, Query, Subscription, Arg, Ctx, Root, Field, ObjectType, InputType, PubSub, registerEnumType } from 'type-graphql';
9
+ import { PubSubEngine } from 'type-graphql';
10
+ import { LogError, LogStatus, UserInfo, Metadata, RunView } from '@memberjunction/core';
11
+ import {
12
+ MCPClientManager,
13
+ MCPSyncToolsResult,
14
+ MCPToolCallResult,
15
+ OAuthAuthorizationRequiredError,
16
+ OAuthReauthorizationRequiredError,
17
+ OAuthManager,
18
+ TokenManager
19
+ } from '@memberjunction/ai-mcp-client';
20
+ import { AppContext } from '../types.js';
21
+ import { ResolverBase } from '../generic/ResolverBase.js';
22
+ import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
23
+ import { GraphQLJSONObject } from 'graphql-type-json';
24
+ import { configInfo } from '../config.js';
25
+
26
+ /**
27
+ * Input type for syncing MCP tools
28
+ */
29
+ @InputType()
30
+ export class SyncMCPToolsInput {
31
+ /**
32
+ * The ID of the MCP server connection to sync tools for
33
+ */
34
+ @Field()
35
+ ConnectionID: string;
36
+
37
+ /**
38
+ * Optional flag to force a full sync even if recently synced
39
+ */
40
+ @Field({ nullable: true })
41
+ ForceSync?: boolean;
42
+ }
43
+
44
+ /**
45
+ * Output type for MCP tool sync results
46
+ */
47
+ @ObjectType()
48
+ export class SyncMCPToolsResult {
49
+ /**
50
+ * Whether the sync operation succeeded
51
+ */
52
+ @Field()
53
+ Success: boolean;
54
+
55
+ /**
56
+ * Error message if the operation failed
57
+ */
58
+ @Field({ nullable: true })
59
+ ErrorMessage?: string;
60
+
61
+ /**
62
+ * Number of tools newly added
63
+ */
64
+ @Field()
65
+ Added: number;
66
+
67
+ /**
68
+ * Number of tools updated
69
+ */
70
+ @Field()
71
+ Updated: number;
72
+
73
+ /**
74
+ * Number of tools marked as deprecated
75
+ */
76
+ @Field()
77
+ Deprecated: number;
78
+
79
+ /**
80
+ * Total number of tools after sync
81
+ */
82
+ @Field()
83
+ Total: number;
84
+
85
+ /**
86
+ * Name of the MCP server that was synced
87
+ */
88
+ @Field({ nullable: true })
89
+ ServerName?: string;
90
+
91
+ /**
92
+ * Connection name that was synced
93
+ */
94
+ @Field({ nullable: true })
95
+ ConnectionName?: string;
96
+
97
+ /**
98
+ * Whether OAuth authorization is required before connecting
99
+ */
100
+ @Field({ nullable: true })
101
+ RequiresOAuth?: boolean;
102
+
103
+ /**
104
+ * OAuth authorization URL if authorization is required
105
+ */
106
+ @Field({ nullable: true })
107
+ AuthorizationUrl?: string;
108
+
109
+ /**
110
+ * OAuth state parameter for tracking the authorization flow
111
+ */
112
+ @Field({ nullable: true })
113
+ StateParameter?: string;
114
+
115
+ /**
116
+ * Whether OAuth re-authorization is required
117
+ */
118
+ @Field({ nullable: true })
119
+ RequiresReauthorization?: boolean;
120
+
121
+ /**
122
+ * Reason for re-authorization if required
123
+ */
124
+ @Field({ nullable: true })
125
+ ReauthorizationReason?: string;
126
+ }
127
+
128
+ /**
129
+ * Input type for executing an MCP tool
130
+ */
131
+ @InputType()
132
+ export class ExecuteMCPToolInput {
133
+ /**
134
+ * The ID of the MCP server connection to use
135
+ */
136
+ @Field()
137
+ ConnectionID: string;
138
+
139
+ /**
140
+ * The ID of the tool to execute (from MCP Server Tools entity)
141
+ */
142
+ @Field()
143
+ ToolID: string;
144
+
145
+ /**
146
+ * The name of the tool to execute
147
+ */
148
+ @Field()
149
+ ToolName: string;
150
+
151
+ /**
152
+ * JSON string of input arguments to pass to the tool
153
+ */
154
+ @Field({ nullable: true })
155
+ InputArgs?: string;
156
+ }
157
+
158
+ /**
159
+ * Output type for MCP tool execution results
160
+ */
161
+ @ObjectType()
162
+ export class ExecuteMCPToolResult {
163
+ /**
164
+ * Whether the tool execution succeeded
165
+ */
166
+ @Field()
167
+ Success: boolean;
168
+
169
+ /**
170
+ * Error message if the execution failed
171
+ */
172
+ @Field({ nullable: true })
173
+ ErrorMessage?: string;
174
+
175
+ /**
176
+ * The result returned by the tool (JSON)
177
+ */
178
+ @Field(() => GraphQLJSONObject, { nullable: true })
179
+ Result?: Record<string, unknown> | null;
180
+
181
+ /**
182
+ * Execution duration in milliseconds
183
+ */
184
+ @Field({ nullable: true })
185
+ DurationMs?: number;
186
+ }
187
+
188
+ /**
189
+ * Progress message type for sync status updates
190
+ */
191
+ interface SyncProgressMessage {
192
+ resolver: string;
193
+ type: string;
194
+ status: 'ok' | 'error';
195
+ connectionId: string;
196
+ phase: 'connecting' | 'listing' | 'syncing' | 'complete' | 'error';
197
+ message: string;
198
+ progress?: {
199
+ current?: number;
200
+ total?: number;
201
+ percentage?: number;
202
+ };
203
+ result?: {
204
+ added: number;
205
+ updated: number;
206
+ deprecated: number;
207
+ total: number;
208
+ };
209
+ }
210
+
211
+ // ============================================================================
212
+ // OAuth GraphQL Types
213
+ // ============================================================================
214
+
215
+ /**
216
+ * OAuth authorization status enum
217
+ */
218
+ enum MCPOAuthStatus {
219
+ PENDING = 'PENDING',
220
+ COMPLETED = 'COMPLETED',
221
+ FAILED = 'FAILED',
222
+ EXPIRED = 'EXPIRED'
223
+ }
224
+
225
+ // Register the enum with TypeGraphQL
226
+ registerEnumType(MCPOAuthStatus, {
227
+ name: 'MCPOAuthStatus',
228
+ description: 'Status of an OAuth authorization flow'
229
+ });
230
+
231
+ /**
232
+ * Input for initiating OAuth authorization
233
+ */
234
+ @InputType()
235
+ export class InitiateMCPOAuthInput {
236
+ @Field()
237
+ ConnectionID: string;
238
+
239
+ @Field({ nullable: true })
240
+ AdditionalScopes?: string;
241
+
242
+ @Field({ nullable: true, description: 'Frontend URL to use as redirect_uri. When provided, the frontend handles the OAuth callback instead of the API server.' })
243
+ FrontendCallbackUrl?: string;
244
+ }
245
+
246
+ /**
247
+ * Input for checking OAuth status
248
+ */
249
+ @InputType()
250
+ export class GetMCPOAuthStatusInput {
251
+ @Field()
252
+ StateParameter: string;
253
+ }
254
+
255
+ /**
256
+ * Input for revoking OAuth credentials
257
+ */
258
+ @InputType()
259
+ export class RevokeMCPOAuthInput {
260
+ @Field()
261
+ ConnectionID: string;
262
+
263
+ @Field({ nullable: true })
264
+ Reason?: string;
265
+ }
266
+
267
+ /**
268
+ * Input for refreshing OAuth tokens
269
+ */
270
+ @InputType()
271
+ export class RefreshMCPOAuthTokenInput {
272
+ @Field()
273
+ ConnectionID: string;
274
+ }
275
+
276
+ /**
277
+ * Result from initiating OAuth authorization
278
+ */
279
+ @ObjectType()
280
+ export class InitiateMCPOAuthResult {
281
+ @Field()
282
+ Success: boolean;
283
+
284
+ @Field({ nullable: true })
285
+ ErrorMessage?: string;
286
+
287
+ @Field({ nullable: true })
288
+ AuthorizationUrl?: string;
289
+
290
+ @Field({ nullable: true })
291
+ StateParameter?: string;
292
+
293
+ @Field({ nullable: true })
294
+ ExpiresAt?: Date;
295
+
296
+ @Field({ nullable: true })
297
+ UsedDynamicRegistration?: boolean;
298
+ }
299
+
300
+ /**
301
+ * Result from checking OAuth status
302
+ */
303
+ @ObjectType()
304
+ export class MCPOAuthStatusResult {
305
+ @Field()
306
+ Success: boolean;
307
+
308
+ @Field({ nullable: true })
309
+ ErrorMessage?: string;
310
+
311
+ @Field(() => MCPOAuthStatus, { nullable: true })
312
+ Status?: MCPOAuthStatus;
313
+
314
+ @Field({ nullable: true })
315
+ ConnectionID?: string;
316
+
317
+ @Field({ nullable: true })
318
+ InitiatedAt?: Date;
319
+
320
+ @Field({ nullable: true })
321
+ CompletedAt?: Date;
322
+
323
+ @Field({ nullable: true })
324
+ AuthErrorCode?: string;
325
+
326
+ @Field({ nullable: true })
327
+ AuthErrorDescription?: string;
328
+
329
+ @Field({ nullable: true })
330
+ IsRetryable?: boolean;
331
+ }
332
+
333
+ /**
334
+ * Result from revoking OAuth credentials
335
+ */
336
+ @ObjectType()
337
+ export class RevokeMCPOAuthResult {
338
+ @Field()
339
+ Success: boolean;
340
+
341
+ @Field({ nullable: true })
342
+ ErrorMessage?: string;
343
+
344
+ @Field({ nullable: true })
345
+ ConnectionID?: string;
346
+ }
347
+
348
+ /**
349
+ * Result from refreshing OAuth tokens
350
+ */
351
+ @ObjectType()
352
+ export class RefreshMCPOAuthTokenResult {
353
+ @Field()
354
+ Success: boolean;
355
+
356
+ @Field({ nullable: true })
357
+ ErrorMessage?: string;
358
+
359
+ @Field({ nullable: true })
360
+ ExpiresAt?: Date;
361
+
362
+ @Field({ nullable: true })
363
+ RequiresReauthorization?: boolean;
364
+ }
365
+
366
+ /**
367
+ * OAuth connection status information
368
+ */
369
+ @ObjectType()
370
+ export class MCPOAuthConnectionStatus {
371
+ @Field()
372
+ ConnectionID: string;
373
+
374
+ @Field()
375
+ IsOAuthEnabled: boolean;
376
+
377
+ @Field()
378
+ HasValidTokens: boolean;
379
+
380
+ @Field({ nullable: true })
381
+ IsAccessTokenExpired?: boolean;
382
+
383
+ @Field({ nullable: true })
384
+ TokenExpiresAt?: Date;
385
+
386
+ @Field({ nullable: true })
387
+ HasRefreshToken?: boolean;
388
+
389
+ @Field()
390
+ RequiresReauthorization: boolean;
391
+
392
+ @Field({ nullable: true })
393
+ ReauthorizationReason?: string;
394
+
395
+ @Field({ nullable: true })
396
+ IssuerUrl?: string;
397
+
398
+ @Field({ nullable: true })
399
+ GrantedScopes?: string;
400
+ }
401
+
402
+ // ========================================
403
+ // Subscription Topics and Types
404
+ // ========================================
405
+
406
+ /** PubSub topic for MCP OAuth events */
407
+ export const MCP_OAUTH_EVENTS_TOPIC = 'MCP_OAUTH_EVENTS';
408
+
409
+ /**
410
+ * OAuth event types for subscriptions
411
+ */
412
+ export type MCPOAuthEventType =
413
+ | 'authorizationRequired'
414
+ | 'authorizationCompleted'
415
+ | 'tokenRefreshed'
416
+ | 'tokenRefreshFailed';
417
+
418
+ /**
419
+ * Payload interface for OAuth subscription events
420
+ */
421
+ export interface MCPOAuthEventPayload {
422
+ eventType: MCPOAuthEventType;
423
+ connectionId: string;
424
+ timestamp: string;
425
+ authorizationUrl?: string;
426
+ stateParameter?: string;
427
+ errorMessage?: string;
428
+ requiresReauthorization?: boolean;
429
+ }
430
+
431
+ /**
432
+ * Notification type for OAuth events
433
+ */
434
+ @ObjectType()
435
+ export class MCPOAuthEventNotification {
436
+ @Field()
437
+ EventType: string;
438
+
439
+ @Field()
440
+ ConnectionID: string;
441
+
442
+ @Field()
443
+ Timestamp: Date;
444
+
445
+ @Field({ nullable: true })
446
+ AuthorizationUrl?: string;
447
+
448
+ @Field({ nullable: true })
449
+ StateParameter?: string;
450
+
451
+ @Field({ nullable: true })
452
+ ErrorMessage?: string;
453
+
454
+ @Field({ nullable: true })
455
+ RequiresReauthorization?: boolean;
456
+ }
457
+
458
+ /**
459
+ * MCP Resolver for GraphQL operations
460
+ */
461
+ @Resolver()
462
+ export class MCPResolver extends ResolverBase {
463
+ /**
464
+ * Syncs tools from an MCP server connection to the database.
465
+ * Publishes progress updates via the statusUpdates subscription.
466
+ *
467
+ * @param input The sync input parameters
468
+ * @param ctx The GraphQL context
469
+ * @param pubSub PubSub engine for progress updates
470
+ * @returns The sync result
471
+ */
472
+ @Mutation(() => SyncMCPToolsResult)
473
+ async SyncMCPTools(
474
+ @Arg('input') input: SyncMCPToolsInput,
475
+ @Ctx() ctx: AppContext,
476
+ @PubSub() pubSub: PubSubEngine
477
+ ): Promise<SyncMCPToolsResult> {
478
+ const user = ctx.userPayload.userRecord;
479
+ if (!user) {
480
+ return this.createErrorResult('User is not authenticated');
481
+ }
482
+
483
+ const { ConnectionID } = input;
484
+ const sessionId = ctx.userPayload.sessionId;
485
+
486
+ try {
487
+ // Check API key scope authorization
488
+ await this.CheckAPIKeyScopeAuthorization('mcp:sync', ConnectionID, ctx.userPayload);
489
+
490
+ // Get the MCP client manager instance and ensure it's initialized
491
+ const manager = MCPClientManager.Instance;
492
+ const publicUrl = this.getPublicUrl();
493
+ await manager.initialize(user, { publicUrl });
494
+
495
+ // Publish initial progress
496
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'connecting', 'Connecting to MCP server...');
497
+
498
+ // Connect if not already connected
499
+ const isConnected = manager.isConnected(ConnectionID);
500
+ if (!isConnected) {
501
+ LogStatus(`MCPResolver: Connecting to MCP server for connection ${ConnectionID}`);
502
+ try {
503
+ await manager.connect(ConnectionID, { contextUser: user });
504
+ } catch (connectError) {
505
+ // Check for OAuth authorization required
506
+ if (connectError instanceof OAuthAuthorizationRequiredError) {
507
+ const authError = connectError as OAuthAuthorizationRequiredError;
508
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'error',
509
+ `OAuth authorization required. Please authorize at: ${authError.authorizationUrl}`);
510
+ return this.createOAuthRequiredResult(authError.authorizationUrl, authError.stateParameter);
511
+ }
512
+ if (connectError instanceof OAuthReauthorizationRequiredError) {
513
+ const reAuthError = connectError as OAuthReauthorizationRequiredError;
514
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'error',
515
+ `OAuth re-authorization required: ${reAuthError.reason}`);
516
+ return this.createOAuthReauthorizationResult(reAuthError.reason, reAuthError.authorizationUrl, reAuthError.stateParameter);
517
+ }
518
+ const connectErrorMsg = connectError instanceof Error ? connectError.message : String(connectError);
519
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Connection failed: ${connectErrorMsg}`);
520
+ return this.createErrorResult(`Failed to connect to MCP server: ${connectErrorMsg}`);
521
+ }
522
+ }
523
+
524
+ // Get connection info for the result
525
+ const connectionInfo = manager.getConnectionInfo(ConnectionID);
526
+ const serverName = connectionInfo?.serverName || 'Unknown Server';
527
+ const connectionName = connectionInfo?.connectionName || 'Unknown Connection';
528
+
529
+ // Publish listing progress
530
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'listing', 'Discovering tools from MCP server...');
531
+
532
+ // Perform the sync with event listening for granular progress
533
+ LogStatus(`MCPResolver: Starting tool sync for connection ${ConnectionID}`);
534
+
535
+ // Subscribe to manager events for this sync
536
+ const eventHandler = (event: { type: string; data?: Record<string, unknown> }) => {
537
+ if (event.type === 'toolsSynced') {
538
+ const data = event.data as { added: number; updated: number; deprecated: number; total: number } | undefined;
539
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'complete', 'Tool sync complete', {
540
+ added: data?.added || 0,
541
+ updated: data?.updated || 0,
542
+ deprecated: data?.deprecated || 0,
543
+ total: data?.total || 0
544
+ });
545
+ }
546
+ };
547
+ manager.addEventListener('toolsSynced', eventHandler);
548
+
549
+ // Publish syncing progress
550
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'syncing', 'Synchronizing tools to database...');
551
+
552
+ // Perform the sync
553
+ const syncResult: MCPSyncToolsResult = await manager.syncTools(ConnectionID, { contextUser: user });
554
+
555
+ // Remove event listener
556
+ manager.removeEventListener('toolsSynced', eventHandler);
557
+
558
+ if (!syncResult.success) {
559
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Sync failed: ${syncResult.error}`);
560
+ return this.createErrorResult(syncResult.error || 'Tool sync failed');
561
+ }
562
+
563
+ // Publish final completion
564
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'complete',
565
+ `Sync complete: ${syncResult.added} added, ${syncResult.updated} updated, ${syncResult.deprecated} deprecated`,
566
+ {
567
+ added: syncResult.added,
568
+ updated: syncResult.updated,
569
+ deprecated: syncResult.deprecated,
570
+ total: syncResult.total
571
+ }
572
+ );
573
+
574
+ LogStatus(`MCPResolver: Tool sync complete for ${ConnectionID} - Added: ${syncResult.added}, Updated: ${syncResult.updated}, Deprecated: ${syncResult.deprecated}, Total: ${syncResult.total}`);
575
+
576
+ return {
577
+ Success: true,
578
+ Added: syncResult.added,
579
+ Updated: syncResult.updated,
580
+ Deprecated: syncResult.deprecated,
581
+ Total: syncResult.total,
582
+ ServerName: serverName,
583
+ ConnectionName: connectionName
584
+ };
585
+ } catch (error) {
586
+ const errorMsg = error instanceof Error ? error.message : String(error);
587
+ LogError(`MCPResolver: Error syncing tools for ${ConnectionID}: ${errorMsg}`);
588
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Error: ${errorMsg}`);
589
+ return this.createErrorResult(errorMsg);
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Executes an MCP tool and returns the result.
595
+ *
596
+ * @param input The execution input parameters
597
+ * @param ctx The GraphQL context
598
+ * @returns The execution result
599
+ */
600
+ @Mutation(() => ExecuteMCPToolResult)
601
+ async ExecuteMCPTool(
602
+ @Arg('input') input: ExecuteMCPToolInput,
603
+ @Ctx() ctx: AppContext
604
+ ): Promise<ExecuteMCPToolResult> {
605
+ const user = ctx.userPayload.userRecord;
606
+ if (!user) {
607
+ return {
608
+ Success: false,
609
+ ErrorMessage: 'User is not authenticated'
610
+ };
611
+ }
612
+
613
+ const { ConnectionID, ToolID, ToolName, InputArgs } = input;
614
+ const startTime = Date.now();
615
+
616
+ try {
617
+ // Check API key scope authorization
618
+ LogStatus(`MCPResolver: [${ToolName}] Step 1 - Checking API key authorization...`);
619
+ await this.CheckAPIKeyScopeAuthorization('mcp:execute', ConnectionID, ctx.userPayload);
620
+ LogStatus(`MCPResolver: [${ToolName}] Step 1 complete - Authorization passed (${Date.now() - startTime}ms)`);
621
+
622
+ // Get the MCP client manager instance and ensure it's initialized
623
+ LogStatus(`MCPResolver: [${ToolName}] Step 2 - Initializing MCP client manager...`);
624
+ const manager = MCPClientManager.Instance;
625
+ const publicUrl = this.getPublicUrl();
626
+ await manager.initialize(user, { publicUrl });
627
+ LogStatus(`MCPResolver: [${ToolName}] Step 2 complete - Manager initialized (${Date.now() - startTime}ms)`);
628
+
629
+ // Connect if not already connected
630
+ const isConnected = manager.isConnected(ConnectionID);
631
+ LogStatus(`MCPResolver: [${ToolName}] Step 3 - Connection status: ${isConnected ? 'already connected' : 'needs connection'}`);
632
+ if (!isConnected) {
633
+ LogStatus(`MCPResolver: [${ToolName}] Connecting to MCP server for connection ${ConnectionID}...`);
634
+ try {
635
+ await manager.connect(ConnectionID, { contextUser: user });
636
+ LogStatus(`MCPResolver: [${ToolName}] Step 3 complete - Connected (${Date.now() - startTime}ms)`);
637
+ } catch (connectError) {
638
+ // Check for OAuth authorization required
639
+ if (connectError instanceof OAuthAuthorizationRequiredError) {
640
+ const authError = connectError as OAuthAuthorizationRequiredError;
641
+ LogError(`MCPResolver: [${ToolName}] OAuth authorization required`);
642
+ return {
643
+ Success: false,
644
+ ErrorMessage: `OAuth authorization required. Please authorize at: ${authError.authorizationUrl}`,
645
+ Result: {
646
+ requiresOAuth: true,
647
+ authorizationUrl: authError.authorizationUrl,
648
+ stateParameter: authError.stateParameter
649
+ }
650
+ };
651
+ }
652
+ if (connectError instanceof OAuthReauthorizationRequiredError) {
653
+ const reAuthError = connectError as OAuthReauthorizationRequiredError;
654
+ LogError(`MCPResolver: [${ToolName}] OAuth re-authorization required: ${reAuthError.reason}`);
655
+ return {
656
+ Success: false,
657
+ ErrorMessage: `OAuth re-authorization required: ${reAuthError.reason}`,
658
+ Result: {
659
+ requiresReauthorization: true,
660
+ reason: reAuthError.reason,
661
+ authorizationUrl: reAuthError.authorizationUrl,
662
+ stateParameter: reAuthError.stateParameter
663
+ }
664
+ };
665
+ }
666
+ const connectErrorMsg = connectError instanceof Error ? connectError.message : String(connectError);
667
+ LogError(`MCPResolver: [${ToolName}] Connection failed: ${connectErrorMsg}`);
668
+ return {
669
+ Success: false,
670
+ ErrorMessage: `Failed to connect to MCP server: ${connectErrorMsg}`
671
+ };
672
+ }
673
+ }
674
+
675
+ // Parse input arguments
676
+ LogStatus(`MCPResolver: [${ToolName}] Step 4 - Parsing input arguments...`);
677
+ let parsedArgs: Record<string, unknown> = {};
678
+ if (InputArgs) {
679
+ try {
680
+ parsedArgs = JSON.parse(InputArgs);
681
+ LogStatus(`MCPResolver: [${ToolName}] Parsed args: ${JSON.stringify(parsedArgs).substring(0, 200)}...`);
682
+ } catch (parseError) {
683
+ LogError(`MCPResolver: [${ToolName}] Failed to parse InputArgs: ${parseError}`);
684
+ return {
685
+ Success: false,
686
+ ErrorMessage: 'Invalid JSON in InputArgs'
687
+ };
688
+ }
689
+ }
690
+ LogStatus(`MCPResolver: [${ToolName}] Step 4 complete - Args parsed (${Date.now() - startTime}ms)`);
691
+
692
+ // Call the tool
693
+ LogStatus(`MCPResolver: [${ToolName}] Step 5 - Calling tool on connection ${ConnectionID}...`);
694
+ LogStatus(`MCPResolver: [${ToolName}] Tool ID: ${ToolID}`);
695
+ const result: MCPToolCallResult = await manager.callTool(
696
+ ConnectionID,
697
+ ToolName,
698
+ { arguments: parsedArgs },
699
+ { contextUser: user }
700
+ );
701
+ LogStatus(`MCPResolver: [${ToolName}] Step 5 complete - Tool call returned (${Date.now() - startTime}ms)`);
702
+
703
+ // Format the result for the response - wrap in object for GraphQLJSONObject
704
+ let formattedResult: Record<string, unknown> | null = null;
705
+ if (result.content && result.content.length > 0) {
706
+ // If there's only one text content block, try to parse as JSON object
707
+ if (result.content.length === 1 && result.content[0].type === 'text') {
708
+ const textContent = result.content[0].text;
709
+ // Try to parse as JSON object
710
+ if (textContent && (textContent.startsWith('{') || textContent.startsWith('['))) {
711
+ try {
712
+ const parsed = JSON.parse(textContent);
713
+ // Wrap arrays in an object
714
+ if (Array.isArray(parsed)) {
715
+ formattedResult = { items: parsed };
716
+ } else if (typeof parsed === 'object' && parsed !== null) {
717
+ formattedResult = parsed as Record<string, unknown>;
718
+ } else {
719
+ formattedResult = { value: parsed };
720
+ }
721
+ } catch {
722
+ // Keep as wrapped string if not valid JSON
723
+ formattedResult = { text: textContent };
724
+ }
725
+ } else {
726
+ // Wrap plain text in object
727
+ formattedResult = { text: textContent };
728
+ }
729
+ } else {
730
+ // Return all content blocks wrapped in object
731
+ formattedResult = { content: result.content };
732
+ }
733
+ }
734
+
735
+ // Use structuredContent if available (already an object)
736
+ if (result.structuredContent) {
737
+ formattedResult = result.structuredContent as Record<string, unknown>;
738
+ }
739
+
740
+ LogStatus(`MCPResolver: [${ToolName}] Step 6 complete - Result formatted (${Date.now() - startTime}ms)`);
741
+ LogStatus(`MCPResolver: [${ToolName}] Tool execution complete - Success: ${result.success}, Duration: ${result.durationMs}ms, Total time: ${Date.now() - startTime}ms`);
742
+
743
+ return {
744
+ Success: result.success,
745
+ ErrorMessage: result.error,
746
+ Result: formattedResult,
747
+ DurationMs: result.durationMs
748
+ };
749
+
750
+ } catch (error) {
751
+ const errorMsg = error instanceof Error ? error.message : String(error);
752
+ const stack = error instanceof Error ? error.stack : '';
753
+ LogError(`MCPResolver: [${ToolName}] Error after ${Date.now() - startTime}ms: ${errorMsg}`);
754
+ LogError(`MCPResolver: [${ToolName}] Stack: ${stack}`);
755
+ return {
756
+ Success: false,
757
+ ErrorMessage: errorMsg
758
+ };
759
+ }
760
+ }
761
+
762
+ // ========================================================================
763
+ // OAuth Operations
764
+ // ========================================================================
765
+
766
+ /**
767
+ * Gets OAuth connection status for an MCP connection.
768
+ *
769
+ * @param connectionId - The MCP Server Connection ID
770
+ * @param ctx - GraphQL context
771
+ * @returns OAuth connection status
772
+ */
773
+ @Query(() => MCPOAuthConnectionStatus)
774
+ async GetMCPOAuthConnectionStatus(
775
+ @Arg('ConnectionID') connectionId: string,
776
+ @Ctx() ctx: AppContext
777
+ ): Promise<MCPOAuthConnectionStatus> {
778
+ const user = ctx.userPayload.userRecord;
779
+ if (!user) {
780
+ return {
781
+ ConnectionID: connectionId,
782
+ IsOAuthEnabled: false,
783
+ HasValidTokens: false,
784
+ RequiresReauthorization: false,
785
+ ReauthorizationReason: 'User is not authenticated'
786
+ };
787
+ }
788
+
789
+ try {
790
+ // Load connection and server config
791
+ const config = await this.loadConnectionOAuthConfig(connectionId, user);
792
+
793
+ if (!config) {
794
+ return {
795
+ ConnectionID: connectionId,
796
+ IsOAuthEnabled: false,
797
+ HasValidTokens: false,
798
+ RequiresReauthorization: false,
799
+ ReauthorizationReason: 'Connection not found'
800
+ };
801
+ }
802
+
803
+ if (!config.OAuthIssuerURL) {
804
+ return {
805
+ ConnectionID: connectionId,
806
+ IsOAuthEnabled: false,
807
+ HasValidTokens: false,
808
+ RequiresReauthorization: false
809
+ };
810
+ }
811
+
812
+ // Get status from OAuthManager
813
+ const oauthManager = new OAuthManager();
814
+ const status = await oauthManager.getConnectionStatus(connectionId, config, user);
815
+
816
+ return {
817
+ ConnectionID: status.connectionId,
818
+ IsOAuthEnabled: status.isOAuthEnabled,
819
+ HasValidTokens: status.hasValidTokens,
820
+ IsAccessTokenExpired: status.isAccessTokenExpired,
821
+ TokenExpiresAt: status.tokenExpiresAt,
822
+ HasRefreshToken: status.hasRefreshToken,
823
+ RequiresReauthorization: status.requiresReauthorization,
824
+ ReauthorizationReason: status.reauthorizationReason,
825
+ IssuerUrl: status.issuerUrl,
826
+ GrantedScopes: status.grantedScopes
827
+ };
828
+ } catch (error) {
829
+ const errorMsg = error instanceof Error ? error.message : String(error);
830
+ LogError(`MCPResolver: GetMCPOAuthConnectionStatus failed: ${errorMsg}`);
831
+ return {
832
+ ConnectionID: connectionId,
833
+ IsOAuthEnabled: false,
834
+ HasValidTokens: false,
835
+ RequiresReauthorization: true,
836
+ ReauthorizationReason: errorMsg
837
+ };
838
+ }
839
+ }
840
+
841
+ /**
842
+ * Gets OAuth authorization flow status by state parameter.
843
+ *
844
+ * @param input - Input containing state parameter
845
+ * @param ctx - GraphQL context
846
+ * @returns OAuth status result
847
+ */
848
+ @Query(() => MCPOAuthStatusResult)
849
+ async GetMCPOAuthStatus(
850
+ @Arg('input') input: GetMCPOAuthStatusInput,
851
+ @Ctx() ctx: AppContext
852
+ ): Promise<MCPOAuthStatusResult> {
853
+ const user = ctx.userPayload.userRecord;
854
+ if (!user) {
855
+ return {
856
+ Success: false,
857
+ ErrorMessage: 'User is not authenticated'
858
+ };
859
+ }
860
+
861
+ try {
862
+ const rv = new RunView();
863
+ const result = await rv.RunView<{
864
+ ID: string;
865
+ MCPServerConnectionID: string;
866
+ Status: string;
867
+ InitiatedAt: Date;
868
+ CompletedAt: Date | null;
869
+ ErrorCode: string | null;
870
+ ErrorDescription: string | null;
871
+ }>({
872
+ EntityName: 'MJ: O Auth Authorization States',
873
+ ExtraFilter: `StateParameter='${input.StateParameter.replace(/'/g, "''")}'`,
874
+ ResultType: 'simple'
875
+ }, user);
876
+
877
+ if (!result.Success || !result.Results || result.Results.length === 0) {
878
+ return {
879
+ Success: false,
880
+ ErrorMessage: 'Authorization state not found',
881
+ IsRetryable: true
882
+ };
883
+ }
884
+
885
+ const state = result.Results[0];
886
+ const statusMap: Record<string, MCPOAuthStatus> = {
887
+ 'Pending': MCPOAuthStatus.PENDING,
888
+ 'Completed': MCPOAuthStatus.COMPLETED,
889
+ 'Failed': MCPOAuthStatus.FAILED,
890
+ 'Expired': MCPOAuthStatus.EXPIRED
891
+ };
892
+
893
+ return {
894
+ Success: true,
895
+ Status: statusMap[state.Status] ?? MCPOAuthStatus.PENDING,
896
+ ConnectionID: state.MCPServerConnectionID,
897
+ InitiatedAt: new Date(state.InitiatedAt),
898
+ CompletedAt: state.CompletedAt ? new Date(state.CompletedAt) : undefined,
899
+ AuthErrorCode: state.ErrorCode ?? undefined,
900
+ AuthErrorDescription: state.ErrorDescription ?? undefined,
901
+ IsRetryable: state.Status === 'Failed' || state.Status === 'Expired'
902
+ };
903
+ } catch (error) {
904
+ const errorMsg = error instanceof Error ? error.message : String(error);
905
+ LogError(`MCPResolver: GetMCPOAuthStatus failed: ${errorMsg}`);
906
+ return {
907
+ Success: false,
908
+ ErrorMessage: errorMsg
909
+ };
910
+ }
911
+ }
912
+
913
+ /**
914
+ * Initiates an OAuth authorization flow for an MCP connection.
915
+ *
916
+ * @param input - Input containing connection ID and optional scopes
917
+ * @param ctx - GraphQL context
918
+ * @returns Initiation result with authorization URL
919
+ */
920
+ @Mutation(() => InitiateMCPOAuthResult)
921
+ async InitiateMCPOAuth(
922
+ @Arg('input') input: InitiateMCPOAuthInput,
923
+ @Ctx() ctx: AppContext
924
+ ): Promise<InitiateMCPOAuthResult> {
925
+ const user = ctx.userPayload.userRecord;
926
+ if (!user) {
927
+ return {
928
+ Success: false,
929
+ ErrorMessage: 'User is not authenticated'
930
+ };
931
+ }
932
+
933
+ try {
934
+ // Check API key scope authorization
935
+ await this.CheckAPIKeyScopeAuthorization('mcp:oauth', input.ConnectionID, ctx.userPayload);
936
+
937
+ // Load connection and server config
938
+ const config = await this.loadConnectionOAuthConfig(input.ConnectionID, user);
939
+
940
+ if (!config) {
941
+ return {
942
+ Success: false,
943
+ ErrorMessage: 'Connection not found'
944
+ };
945
+ }
946
+
947
+ if (!config.OAuthIssuerURL) {
948
+ return {
949
+ Success: false,
950
+ ErrorMessage: 'OAuth is not configured for this connection'
951
+ };
952
+ }
953
+
954
+ // Merge additional scopes if provided
955
+ let scopes = config.OAuthScopes;
956
+ if (input.AdditionalScopes) {
957
+ scopes = scopes
958
+ ? `${scopes} ${input.AdditionalScopes}`
959
+ : input.AdditionalScopes;
960
+ }
961
+
962
+ const oauthConfig = {
963
+ ...config,
964
+ OAuthScopes: scopes
965
+ };
966
+
967
+ // Initiate the OAuth flow
968
+ const oauthManager = new OAuthManager();
969
+ const publicUrl = this.getPublicUrl();
970
+
971
+ // Build options for the OAuth flow
972
+ const oauthOptions: { frontendReturnUrl?: string; frontendCallbackUrl?: string } = {};
973
+ if (input.FrontendCallbackUrl) {
974
+ oauthOptions.frontendCallbackUrl = input.FrontendCallbackUrl;
975
+ }
976
+
977
+ const result = await oauthManager.initiateAuthorizationFlow(
978
+ input.ConnectionID,
979
+ config.MCPServerID,
980
+ oauthConfig,
981
+ publicUrl,
982
+ user,
983
+ Object.keys(oauthOptions).length > 0 ? oauthOptions : undefined
984
+ );
985
+
986
+ LogStatus(`MCPResolver: Initiated OAuth flow for connection ${input.ConnectionID}`);
987
+
988
+ return {
989
+ Success: result.success,
990
+ ErrorMessage: result.errorMessage,
991
+ AuthorizationUrl: result.authorizationUrl,
992
+ StateParameter: result.stateParameter,
993
+ ExpiresAt: result.expiresAt,
994
+ UsedDynamicRegistration: result.usedDynamicRegistration
995
+ };
996
+ } catch (error) {
997
+ const errorMsg = error instanceof Error ? error.message : String(error);
998
+ LogError(`MCPResolver: InitiateMCPOAuth failed: ${errorMsg}`);
999
+ return {
1000
+ Success: false,
1001
+ ErrorMessage: errorMsg
1002
+ };
1003
+ }
1004
+ }
1005
+
1006
+ /**
1007
+ * Revokes OAuth credentials for an MCP connection.
1008
+ *
1009
+ * @param input - Input containing connection ID and optional reason
1010
+ * @param ctx - GraphQL context
1011
+ * @returns Revocation result
1012
+ */
1013
+ @Mutation(() => RevokeMCPOAuthResult)
1014
+ async RevokeMCPOAuth(
1015
+ @Arg('input') input: RevokeMCPOAuthInput,
1016
+ @Ctx() ctx: AppContext
1017
+ ): Promise<RevokeMCPOAuthResult> {
1018
+ const user = ctx.userPayload.userRecord;
1019
+ if (!user) {
1020
+ return {
1021
+ Success: false,
1022
+ ErrorMessage: 'User is not authenticated'
1023
+ };
1024
+ }
1025
+
1026
+ try {
1027
+ // Check API key scope authorization
1028
+ await this.CheckAPIKeyScopeAuthorization('mcp:oauth', input.ConnectionID, ctx.userPayload);
1029
+
1030
+ // Revoke the credentials
1031
+ const tokenManager = new TokenManager();
1032
+ await tokenManager.revokeCredentials(input.ConnectionID, user);
1033
+
1034
+ LogStatus(`MCPResolver: Revoked OAuth credentials for connection ${input.ConnectionID}${input.Reason ? ` (reason: ${input.Reason})` : ''}`);
1035
+
1036
+ return {
1037
+ Success: true,
1038
+ ConnectionID: input.ConnectionID
1039
+ };
1040
+ } catch (error) {
1041
+ const errorMsg = error instanceof Error ? error.message : String(error);
1042
+ LogError(`MCPResolver: RevokeMCPOAuth failed: ${errorMsg}`);
1043
+ return {
1044
+ Success: false,
1045
+ ErrorMessage: errorMsg,
1046
+ ConnectionID: input.ConnectionID
1047
+ };
1048
+ }
1049
+ }
1050
+
1051
+ /**
1052
+ * Manually refreshes OAuth tokens for an MCP connection.
1053
+ *
1054
+ * @param input - Input containing connection ID
1055
+ * @param ctx - GraphQL context
1056
+ * @returns Refresh result
1057
+ */
1058
+ @Mutation(() => RefreshMCPOAuthTokenResult)
1059
+ async RefreshMCPOAuthToken(
1060
+ @Arg('input') input: RefreshMCPOAuthTokenInput,
1061
+ @Ctx() ctx: AppContext
1062
+ ): Promise<RefreshMCPOAuthTokenResult> {
1063
+ const user = ctx.userPayload.userRecord;
1064
+ if (!user) {
1065
+ return {
1066
+ Success: false,
1067
+ ErrorMessage: 'User is not authenticated'
1068
+ };
1069
+ }
1070
+
1071
+ try {
1072
+ // Check API key scope authorization
1073
+ await this.CheckAPIKeyScopeAuthorization('mcp:oauth', input.ConnectionID, ctx.userPayload);
1074
+
1075
+ // Load connection config
1076
+ const config = await this.loadConnectionOAuthConfig(input.ConnectionID, user);
1077
+
1078
+ if (!config) {
1079
+ return {
1080
+ Success: false,
1081
+ ErrorMessage: 'Connection not found'
1082
+ };
1083
+ }
1084
+
1085
+ if (!config.OAuthIssuerURL) {
1086
+ return {
1087
+ Success: false,
1088
+ ErrorMessage: 'OAuth is not configured for this connection'
1089
+ };
1090
+ }
1091
+
1092
+ // Get the MCP client manager instance
1093
+ const manager = MCPClientManager.Instance;
1094
+ const publicUrl = this.getPublicUrl();
1095
+ await manager.initialize(user, { publicUrl });
1096
+
1097
+ // Try to get access token (will refresh if needed)
1098
+ const oauthManager = new OAuthManager();
1099
+ try {
1100
+ await oauthManager.getAccessToken(
1101
+ input.ConnectionID,
1102
+ config.MCPServerID,
1103
+ config,
1104
+ publicUrl,
1105
+ user
1106
+ );
1107
+
1108
+ // Get updated status
1109
+ const status = await oauthManager.getConnectionStatus(input.ConnectionID, config, user);
1110
+
1111
+ LogStatus(`MCPResolver: Refreshed OAuth tokens for connection ${input.ConnectionID}`);
1112
+
1113
+ return {
1114
+ Success: true,
1115
+ ExpiresAt: status.tokenExpiresAt,
1116
+ RequiresReauthorization: false
1117
+ };
1118
+ } catch (error) {
1119
+ if (error instanceof OAuthAuthorizationRequiredError ||
1120
+ error instanceof OAuthReauthorizationRequiredError) {
1121
+ return {
1122
+ Success: false,
1123
+ ErrorMessage: error.message,
1124
+ RequiresReauthorization: true
1125
+ };
1126
+ }
1127
+ throw error;
1128
+ }
1129
+ } catch (error) {
1130
+ const errorMsg = error instanceof Error ? error.message : String(error);
1131
+ LogError(`MCPResolver: RefreshMCPOAuthToken failed: ${errorMsg}`);
1132
+ return {
1133
+ Success: false,
1134
+ ErrorMessage: errorMsg
1135
+ };
1136
+ }
1137
+ }
1138
+
1139
+ // ========================================================================
1140
+ // Subscriptions
1141
+ // ========================================================================
1142
+
1143
+ /**
1144
+ * Subscribes to OAuth events for MCP connections.
1145
+ *
1146
+ * Clients can subscribe to receive real-time notifications when:
1147
+ * - Authorization is required for a connection
1148
+ * - Authorization completes successfully
1149
+ * - Token is refreshed
1150
+ * - Token refresh fails
1151
+ *
1152
+ * @param payload - The OAuth event payload
1153
+ * @returns OAuth event notification
1154
+ */
1155
+ @Subscription(() => MCPOAuthEventNotification, { topics: MCP_OAUTH_EVENTS_TOPIC })
1156
+ onMCPOAuthEvent(
1157
+ @Root() payload: MCPOAuthEventPayload
1158
+ ): MCPOAuthEventNotification {
1159
+ return {
1160
+ EventType: payload.eventType,
1161
+ ConnectionID: payload.connectionId,
1162
+ Timestamp: new Date(payload.timestamp),
1163
+ AuthorizationUrl: payload.authorizationUrl,
1164
+ StateParameter: payload.stateParameter,
1165
+ ErrorMessage: payload.errorMessage,
1166
+ RequiresReauthorization: payload.requiresReauthorization
1167
+ };
1168
+ }
1169
+
1170
+ // ========================================================================
1171
+ // Private Helper Methods
1172
+ // ========================================================================
1173
+
1174
+ /**
1175
+ * Loads OAuth configuration for a connection
1176
+ */
1177
+ private async loadConnectionOAuthConfig(
1178
+ connectionId: string,
1179
+ contextUser: UserInfo
1180
+ ): Promise<{
1181
+ MCPServerID: string;
1182
+ OAuthIssuerURL?: string;
1183
+ OAuthScopes?: string;
1184
+ OAuthMetadataCacheTTLMinutes?: number;
1185
+ OAuthClientID?: string;
1186
+ OAuthClientSecretEncrypted?: string;
1187
+ OAuthRequirePKCE?: boolean;
1188
+ } | null> {
1189
+ try {
1190
+ const rv = new RunView();
1191
+
1192
+ // First get connection to get server ID
1193
+ const connResult = await rv.RunView<{ MCPServerID: string }>({
1194
+ EntityName: 'MJ: MCP Server Connections',
1195
+ ExtraFilter: `ID='${connectionId}'`,
1196
+ Fields: ['MCPServerID'],
1197
+ ResultType: 'simple'
1198
+ }, contextUser);
1199
+
1200
+ if (!connResult.Success || !connResult.Results || connResult.Results.length === 0) {
1201
+ return null;
1202
+ }
1203
+
1204
+ const serverId = connResult.Results[0].MCPServerID;
1205
+
1206
+ // Then get server OAuth config
1207
+ const serverResult = await rv.RunView<{
1208
+ OAuthIssuerURL: string | null;
1209
+ OAuthScopes: string | null;
1210
+ OAuthMetadataCacheTTLMinutes: number | null;
1211
+ OAuthClientID: string | null;
1212
+ OAuthClientSecretEncrypted: string | null;
1213
+ OAuthRequirePKCE: boolean | null;
1214
+ }>({
1215
+ EntityName: 'MJ: MCP Servers',
1216
+ ExtraFilter: `ID='${serverId}'`,
1217
+ Fields: [
1218
+ 'OAuthIssuerURL',
1219
+ 'OAuthScopes',
1220
+ 'OAuthMetadataCacheTTLMinutes',
1221
+ 'OAuthClientID',
1222
+ 'OAuthClientSecretEncrypted',
1223
+ 'OAuthRequirePKCE'
1224
+ ],
1225
+ ResultType: 'simple'
1226
+ }, contextUser);
1227
+
1228
+ if (!serverResult.Success || !serverResult.Results || serverResult.Results.length === 0) {
1229
+ return null;
1230
+ }
1231
+
1232
+ const server = serverResult.Results[0];
1233
+ return {
1234
+ MCPServerID: serverId,
1235
+ OAuthIssuerURL: server.OAuthIssuerURL ?? undefined,
1236
+ OAuthScopes: server.OAuthScopes ?? undefined,
1237
+ OAuthMetadataCacheTTLMinutes: server.OAuthMetadataCacheTTLMinutes ?? undefined,
1238
+ OAuthClientID: server.OAuthClientID ?? undefined,
1239
+ OAuthClientSecretEncrypted: server.OAuthClientSecretEncrypted ?? undefined,
1240
+ OAuthRequirePKCE: server.OAuthRequirePKCE ?? undefined
1241
+ };
1242
+ } catch (error) {
1243
+ LogError(`MCPResolver: Failed to load connection OAuth config: ${error}`);
1244
+ return null;
1245
+ }
1246
+ }
1247
+
1248
+ /**
1249
+ * Publishes a progress update to the statusUpdates subscription
1250
+ */
1251
+ private publishProgress(
1252
+ pubSub: PubSubEngine,
1253
+ sessionId: string,
1254
+ connectionId: string,
1255
+ phase: SyncProgressMessage['phase'],
1256
+ message: string,
1257
+ result?: { added: number; updated: number; deprecated: number; total: number }
1258
+ ): void {
1259
+ const progressMessage: SyncProgressMessage = {
1260
+ resolver: 'MCPResolver',
1261
+ type: 'MCPToolSyncProgress',
1262
+ status: phase === 'error' ? 'error' : 'ok',
1263
+ connectionId,
1264
+ phase,
1265
+ message,
1266
+ result
1267
+ };
1268
+
1269
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
1270
+ message: JSON.stringify(progressMessage),
1271
+ sessionId
1272
+ });
1273
+ }
1274
+
1275
+ /**
1276
+ * Creates an error result with default values
1277
+ */
1278
+ private createErrorResult(errorMessage: string): SyncMCPToolsResult {
1279
+ return {
1280
+ Success: false,
1281
+ ErrorMessage: errorMessage,
1282
+ Added: 0,
1283
+ Updated: 0,
1284
+ Deprecated: 0,
1285
+ Total: 0
1286
+ };
1287
+ }
1288
+
1289
+ /**
1290
+ * Creates a result indicating OAuth authorization is required
1291
+ */
1292
+ private createOAuthRequiredResult(authorizationUrl: string, stateParameter: string): SyncMCPToolsResult {
1293
+ return {
1294
+ Success: false,
1295
+ ErrorMessage: 'OAuth authorization required',
1296
+ Added: 0,
1297
+ Updated: 0,
1298
+ Deprecated: 0,
1299
+ Total: 0,
1300
+ RequiresOAuth: true,
1301
+ AuthorizationUrl: authorizationUrl,
1302
+ StateParameter: stateParameter
1303
+ };
1304
+ }
1305
+
1306
+ /**
1307
+ * Creates a result indicating OAuth re-authorization is required
1308
+ */
1309
+ private createOAuthReauthorizationResult(
1310
+ reason: string,
1311
+ authorizationUrl?: string,
1312
+ stateParameter?: string
1313
+ ): SyncMCPToolsResult {
1314
+ return {
1315
+ Success: false,
1316
+ ErrorMessage: `OAuth re-authorization required: ${reason}`,
1317
+ Added: 0,
1318
+ Updated: 0,
1319
+ Deprecated: 0,
1320
+ Total: 0,
1321
+ RequiresReauthorization: true,
1322
+ ReauthorizationReason: reason,
1323
+ AuthorizationUrl: authorizationUrl,
1324
+ StateParameter: stateParameter
1325
+ };
1326
+ }
1327
+
1328
+ /**
1329
+ * Gets the public URL for OAuth callbacks
1330
+ */
1331
+ private getPublicUrl(): string {
1332
+ // Use publicUrl from config, falling back to constructed URL
1333
+ if (configInfo.publicUrl) {
1334
+ return configInfo.publicUrl;
1335
+ }
1336
+
1337
+ // Construct from baseUrl and graphqlPort
1338
+ const baseUrl = configInfo.baseUrl || 'http://localhost';
1339
+ const port = configInfo.graphqlPort || 4000;
1340
+ const rootPath = configInfo.graphqlRootPath || '/';
1341
+
1342
+ // Construct full URL
1343
+ let url = `${baseUrl}:${port}`;
1344
+ if (rootPath && rootPath !== '/') {
1345
+ url += rootPath;
1346
+ }
1347
+
1348
+ return url;
1349
+ }
1350
+ }
1351
+ /**
1352
+ * Publishes an OAuth event to the subscription topic.
1353
+ * Can be called from other resolvers or handlers to notify clients of OAuth events.
1354
+ *
1355
+ * @param pubSub - PubSub engine
1356
+ * @param event - OAuth event details
1357
+ */
1358
+ export async function publishMCPOAuthEvent(
1359
+ pubSub: PubSubEngine,
1360
+ event: {
1361
+ eventType: MCPOAuthEventType;
1362
+ connectionId: string;
1363
+ authorizationUrl?: string;
1364
+ stateParameter?: string;
1365
+ errorMessage?: string;
1366
+ requiresReauthorization?: boolean;
1367
+ }
1368
+ ): Promise<void> {
1369
+ const payload: MCPOAuthEventPayload = {
1370
+ eventType: event.eventType,
1371
+ connectionId: event.connectionId,
1372
+ timestamp: new Date().toISOString(),
1373
+ authorizationUrl: event.authorizationUrl,
1374
+ stateParameter: event.stateParameter,
1375
+ errorMessage: event.errorMessage,
1376
+ requiresReauthorization: event.requiresReauthorization
1377
+ };
1378
+
1379
+ await pubSub.publish(MCP_OAUTH_EVENTS_TOPIC, payload);
1380
+ }