@memberjunction/server 3.2.0 → 3.4.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 (127) hide show
  1. package/README.md +106 -1
  2. package/dist/auth/APIKeyScopeAuth.d.ts +51 -0
  3. package/dist/auth/APIKeyScopeAuth.d.ts.map +1 -0
  4. package/dist/auth/APIKeyScopeAuth.js +163 -0
  5. package/dist/auth/APIKeyScopeAuth.js.map +1 -0
  6. package/dist/auth/BaseAuthProvider.d.ts +1 -0
  7. package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
  8. package/dist/auth/BaseAuthProvider.js +2 -0
  9. package/dist/auth/BaseAuthProvider.js.map +1 -1
  10. package/dist/auth/IAuthProvider.d.ts +1 -0
  11. package/dist/auth/IAuthProvider.d.ts.map +1 -1
  12. package/dist/auth/index.d.ts +1 -0
  13. package/dist/auth/index.d.ts.map +1 -1
  14. package/dist/auth/index.js +1 -0
  15. package/dist/auth/index.js.map +1 -1
  16. package/dist/config.js +2 -2
  17. package/dist/config.js.map +1 -1
  18. package/dist/context.d.ts +8 -1
  19. package/dist/context.d.ts.map +1 -1
  20. package/dist/context.js +44 -7
  21. package/dist/context.js.map +1 -1
  22. package/dist/generated/generated.d.ts +681 -2
  23. package/dist/generated/generated.d.ts.map +1 -1
  24. package/dist/generated/generated.js +10627 -6409
  25. package/dist/generated/generated.js.map +1 -1
  26. package/dist/generic/ResolverBase.d.ts +3 -2
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +52 -4
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/generic/RunViewResolver.d.ts +29 -1
  31. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  32. package/dist/generic/RunViewResolver.js +143 -0
  33. package/dist/generic/RunViewResolver.js.map +1 -1
  34. package/dist/index.d.ts +4 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/resolvers/APIKeyResolver.d.ts +24 -0
  39. package/dist/resolvers/APIKeyResolver.d.ts.map +1 -0
  40. package/dist/resolvers/APIKeyResolver.js +194 -0
  41. package/dist/resolvers/APIKeyResolver.js.map +1 -0
  42. package/dist/resolvers/ActionResolver.d.ts +2 -1
  43. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  44. package/dist/resolvers/ActionResolver.js +4 -1
  45. package/dist/resolvers/ActionResolver.js.map +1 -1
  46. package/dist/resolvers/DatasetResolver.d.ts +5 -4
  47. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  48. package/dist/resolvers/DatasetResolver.js +7 -4
  49. package/dist/resolvers/DatasetResolver.js.map +1 -1
  50. package/dist/resolvers/EntityCommunicationsResolver.d.ts +2 -1
  51. package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
  52. package/dist/resolvers/EntityCommunicationsResolver.js +3 -1
  53. package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
  54. package/dist/resolvers/GetDataContextDataResolver.d.ts +2 -1
  55. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  56. package/dist/resolvers/GetDataContextDataResolver.js +10 -3
  57. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  58. package/dist/resolvers/MCPResolver.d.ts +37 -0
  59. package/dist/resolvers/MCPResolver.d.ts.map +1 -0
  60. package/dist/resolvers/MCPResolver.js +363 -0
  61. package/dist/resolvers/MCPResolver.js.map +1 -0
  62. package/dist/resolvers/MergeRecordsResolver.d.ts +2 -1
  63. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  64. package/dist/resolvers/MergeRecordsResolver.js +3 -1
  65. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  66. package/dist/resolvers/QueryResolver.d.ts +2 -1
  67. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  68. package/dist/resolvers/QueryResolver.js +6 -1
  69. package/dist/resolvers/QueryResolver.js.map +1 -1
  70. package/dist/resolvers/ReportResolver.d.ts +2 -1
  71. package/dist/resolvers/ReportResolver.d.ts.map +1 -1
  72. package/dist/resolvers/ReportResolver.js +4 -1
  73. package/dist/resolvers/ReportResolver.js.map +1 -1
  74. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  75. package/dist/resolvers/RunAIAgentResolver.js +3 -1
  76. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  77. package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
  78. package/dist/resolvers/RunAIPromptResolver.js +3 -0
  79. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  80. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  81. package/dist/resolvers/RunTemplateResolver.js +1 -0
  82. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  83. package/dist/resolvers/TaskResolver.d.ts.map +1 -1
  84. package/dist/resolvers/TaskResolver.js +1 -0
  85. package/dist/resolvers/TaskResolver.js.map +1 -1
  86. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  87. package/dist/resolvers/UserResolver.js +35 -1
  88. package/dist/resolvers/UserResolver.js.map +1 -1
  89. package/dist/types.d.ts +4 -1
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/types.js.map +1 -1
  92. package/package.json +47 -45
  93. package/src/auth/APIKeyScopeAuth.ts +366 -0
  94. package/src/auth/BaseAuthProvider.ts +3 -0
  95. package/src/auth/IAuthProvider.ts +5 -0
  96. package/src/auth/index.ts +1 -0
  97. package/src/config.ts +2 -2
  98. package/src/context.ts +91 -9
  99. package/src/generated/generated.ts +6327 -3668
  100. package/src/generic/ResolverBase.ts +127 -8
  101. package/src/generic/RunViewResolver.ts +132 -5
  102. package/src/index.ts +12 -2
  103. package/src/resolvers/APIKeyResolver.ts +241 -0
  104. package/src/resolvers/ActionResolver.ts +8 -1
  105. package/src/resolvers/DatasetResolver.ts +11 -4
  106. package/src/resolvers/EntityCommunicationsResolver.ts +5 -1
  107. package/src/resolvers/GetDataContextDataResolver.ts +14 -6
  108. package/src/resolvers/MCPResolver.ts +480 -0
  109. package/src/resolvers/MergeRecordsResolver.ts +5 -1
  110. package/src/resolvers/QueryResolver.ts +17 -3
  111. package/src/resolvers/ReportResolver.ts +8 -1
  112. package/src/resolvers/RunAIAgentResolver.ts +7 -1
  113. package/src/resolvers/RunAIPromptResolver.ts +10 -1
  114. package/src/resolvers/RunTemplateResolver.ts +4 -1
  115. package/src/resolvers/TaskResolver.ts +3 -0
  116. package/src/resolvers/UserResolver.ts +52 -4
  117. package/src/types.ts +7 -2
  118. package/dist/resolvers/AskSkipResolver.d.ts +0 -123
  119. package/dist/resolvers/AskSkipResolver.d.ts.map +0 -1
  120. package/dist/resolvers/AskSkipResolver.js +0 -1788
  121. package/dist/resolvers/AskSkipResolver.js.map +0 -1
  122. package/dist/scheduler/LearningCycleScheduler.d.ts +0 -4
  123. package/dist/scheduler/LearningCycleScheduler.d.ts.map +0 -1
  124. package/dist/scheduler/LearningCycleScheduler.js +0 -4
  125. package/dist/scheduler/LearningCycleScheduler.js.map +0 -1
  126. package/src/resolvers/AskSkipResolver.ts +0 -3446
  127. package/src/scheduler/LearningCycleScheduler.ts +0 -320
@@ -0,0 +1,480 @@
1
+ /**
2
+ * @fileoverview MCP GraphQL Resolver
3
+ *
4
+ * Provides GraphQL mutations for MCP (Model Context Protocol) operations
5
+ * including tool synchronization with progress streaming.
6
+ */
7
+
8
+ import { Resolver, Mutation, Arg, Ctx, Field, ObjectType, InputType, PubSub } from 'type-graphql';
9
+ import { PubSubEngine } from 'type-graphql';
10
+ import { LogError, LogStatus, UserInfo } from '@memberjunction/core';
11
+ import { MCPClientManager, MCPSyncToolsResult, MCPToolCallResult } from '@memberjunction/ai-mcp-client';
12
+ import { AppContext } from '../types.js';
13
+ import { ResolverBase } from '../generic/ResolverBase.js';
14
+ import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
15
+ import { GraphQLJSONObject } from 'graphql-type-json';
16
+
17
+ /**
18
+ * Input type for syncing MCP tools
19
+ */
20
+ @InputType()
21
+ export class SyncMCPToolsInput {
22
+ /**
23
+ * The ID of the MCP server connection to sync tools for
24
+ */
25
+ @Field()
26
+ ConnectionID: string;
27
+
28
+ /**
29
+ * Optional flag to force a full sync even if recently synced
30
+ */
31
+ @Field({ nullable: true })
32
+ ForceSync?: boolean;
33
+ }
34
+
35
+ /**
36
+ * Output type for MCP tool sync results
37
+ */
38
+ @ObjectType()
39
+ export class SyncMCPToolsResult {
40
+ /**
41
+ * Whether the sync operation succeeded
42
+ */
43
+ @Field()
44
+ Success: boolean;
45
+
46
+ /**
47
+ * Error message if the operation failed
48
+ */
49
+ @Field({ nullable: true })
50
+ ErrorMessage?: string;
51
+
52
+ /**
53
+ * Number of tools newly added
54
+ */
55
+ @Field()
56
+ Added: number;
57
+
58
+ /**
59
+ * Number of tools updated
60
+ */
61
+ @Field()
62
+ Updated: number;
63
+
64
+ /**
65
+ * Number of tools marked as deprecated
66
+ */
67
+ @Field()
68
+ Deprecated: number;
69
+
70
+ /**
71
+ * Total number of tools after sync
72
+ */
73
+ @Field()
74
+ Total: number;
75
+
76
+ /**
77
+ * Name of the MCP server that was synced
78
+ */
79
+ @Field({ nullable: true })
80
+ ServerName?: string;
81
+
82
+ /**
83
+ * Connection name that was synced
84
+ */
85
+ @Field({ nullable: true })
86
+ ConnectionName?: string;
87
+ }
88
+
89
+ /**
90
+ * Input type for executing an MCP tool
91
+ */
92
+ @InputType()
93
+ export class ExecuteMCPToolInput {
94
+ /**
95
+ * The ID of the MCP server connection to use
96
+ */
97
+ @Field()
98
+ ConnectionID: string;
99
+
100
+ /**
101
+ * The ID of the tool to execute (from MCP Server Tools entity)
102
+ */
103
+ @Field()
104
+ ToolID: string;
105
+
106
+ /**
107
+ * The name of the tool to execute
108
+ */
109
+ @Field()
110
+ ToolName: string;
111
+
112
+ /**
113
+ * JSON string of input arguments to pass to the tool
114
+ */
115
+ @Field({ nullable: true })
116
+ InputArgs?: string;
117
+ }
118
+
119
+ /**
120
+ * Output type for MCP tool execution results
121
+ */
122
+ @ObjectType()
123
+ export class ExecuteMCPToolResult {
124
+ /**
125
+ * Whether the tool execution succeeded
126
+ */
127
+ @Field()
128
+ Success: boolean;
129
+
130
+ /**
131
+ * Error message if the execution failed
132
+ */
133
+ @Field({ nullable: true })
134
+ ErrorMessage?: string;
135
+
136
+ /**
137
+ * The result returned by the tool (JSON)
138
+ */
139
+ @Field(() => GraphQLJSONObject, { nullable: true })
140
+ Result?: Record<string, unknown> | null;
141
+
142
+ /**
143
+ * Execution duration in milliseconds
144
+ */
145
+ @Field({ nullable: true })
146
+ DurationMs?: number;
147
+ }
148
+
149
+ /**
150
+ * Progress message type for sync status updates
151
+ */
152
+ interface SyncProgressMessage {
153
+ resolver: string;
154
+ type: string;
155
+ status: 'ok' | 'error';
156
+ connectionId: string;
157
+ phase: 'connecting' | 'listing' | 'syncing' | 'complete' | 'error';
158
+ message: string;
159
+ progress?: {
160
+ current?: number;
161
+ total?: number;
162
+ percentage?: number;
163
+ };
164
+ result?: {
165
+ added: number;
166
+ updated: number;
167
+ deprecated: number;
168
+ total: number;
169
+ };
170
+ }
171
+
172
+ /**
173
+ * MCP Resolver for GraphQL operations
174
+ */
175
+ @Resolver()
176
+ export class MCPResolver extends ResolverBase {
177
+ /**
178
+ * Syncs tools from an MCP server connection to the database.
179
+ * Publishes progress updates via the statusUpdates subscription.
180
+ *
181
+ * @param input The sync input parameters
182
+ * @param ctx The GraphQL context
183
+ * @param pubSub PubSub engine for progress updates
184
+ * @returns The sync result
185
+ */
186
+ @Mutation(() => SyncMCPToolsResult)
187
+ async SyncMCPTools(
188
+ @Arg('input') input: SyncMCPToolsInput,
189
+ @Ctx() ctx: AppContext,
190
+ @PubSub() pubSub: PubSubEngine
191
+ ): Promise<SyncMCPToolsResult> {
192
+ const user = ctx.userPayload.userRecord;
193
+ if (!user) {
194
+ return this.createErrorResult('User is not authenticated');
195
+ }
196
+
197
+ const { ConnectionID } = input;
198
+ const sessionId = ctx.userPayload.sessionId;
199
+
200
+ try {
201
+ // Check API key scope authorization
202
+ await this.CheckAPIKeyScopeAuthorization('mcp:sync', ConnectionID, ctx.userPayload);
203
+
204
+ // Get the MCP client manager instance and ensure it's initialized
205
+ const manager = MCPClientManager.Instance;
206
+ await manager.initialize(user);
207
+
208
+ // Publish initial progress
209
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'connecting', 'Connecting to MCP server...');
210
+
211
+ // Connect if not already connected
212
+ const isConnected = manager.isConnected(ConnectionID);
213
+ if (!isConnected) {
214
+ LogStatus(`MCPResolver: Connecting to MCP server for connection ${ConnectionID}`);
215
+ try {
216
+ await manager.connect(ConnectionID, { contextUser: user });
217
+ } catch (connectError) {
218
+ const connectErrorMsg = connectError instanceof Error ? connectError.message : String(connectError);
219
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Connection failed: ${connectErrorMsg}`);
220
+ return this.createErrorResult(`Failed to connect to MCP server: ${connectErrorMsg}`);
221
+ }
222
+ }
223
+
224
+ // Get connection info for the result
225
+ const connectionInfo = manager.getConnectionInfo(ConnectionID);
226
+ const serverName = connectionInfo?.serverName || 'Unknown Server';
227
+ const connectionName = connectionInfo?.connectionName || 'Unknown Connection';
228
+
229
+ // Publish listing progress
230
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'listing', 'Discovering tools from MCP server...');
231
+
232
+ // Perform the sync with event listening for granular progress
233
+ LogStatus(`MCPResolver: Starting tool sync for connection ${ConnectionID}`);
234
+
235
+ // Subscribe to manager events for this sync
236
+ const eventHandler = (event: { type: string; data?: Record<string, unknown> }) => {
237
+ if (event.type === 'toolsSynced') {
238
+ const data = event.data as { added: number; updated: number; deprecated: number; total: number } | undefined;
239
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'complete', 'Tool sync complete', {
240
+ added: data?.added || 0,
241
+ updated: data?.updated || 0,
242
+ deprecated: data?.deprecated || 0,
243
+ total: data?.total || 0
244
+ });
245
+ }
246
+ };
247
+ manager.addEventListener('toolsSynced', eventHandler);
248
+
249
+ // Publish syncing progress
250
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'syncing', 'Synchronizing tools to database...');
251
+
252
+ // Perform the sync
253
+ const syncResult: MCPSyncToolsResult = await manager.syncTools(ConnectionID, { contextUser: user });
254
+
255
+ // Remove event listener
256
+ manager.removeEventListener('toolsSynced', eventHandler);
257
+
258
+ if (!syncResult.success) {
259
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Sync failed: ${syncResult.error}`);
260
+ return this.createErrorResult(syncResult.error || 'Tool sync failed');
261
+ }
262
+
263
+ // Publish final completion
264
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'complete',
265
+ `Sync complete: ${syncResult.added} added, ${syncResult.updated} updated, ${syncResult.deprecated} deprecated`,
266
+ {
267
+ added: syncResult.added,
268
+ updated: syncResult.updated,
269
+ deprecated: syncResult.deprecated,
270
+ total: syncResult.total
271
+ }
272
+ );
273
+
274
+ LogStatus(`MCPResolver: Tool sync complete for ${ConnectionID} - Added: ${syncResult.added}, Updated: ${syncResult.updated}, Deprecated: ${syncResult.deprecated}, Total: ${syncResult.total}`);
275
+
276
+ return {
277
+ Success: true,
278
+ Added: syncResult.added,
279
+ Updated: syncResult.updated,
280
+ Deprecated: syncResult.deprecated,
281
+ Total: syncResult.total,
282
+ ServerName: serverName,
283
+ ConnectionName: connectionName
284
+ };
285
+ } catch (error) {
286
+ const errorMsg = error instanceof Error ? error.message : String(error);
287
+ LogError(`MCPResolver: Error syncing tools for ${ConnectionID}: ${errorMsg}`);
288
+ this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Error: ${errorMsg}`);
289
+ return this.createErrorResult(errorMsg);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Executes an MCP tool and returns the result.
295
+ *
296
+ * @param input The execution input parameters
297
+ * @param ctx The GraphQL context
298
+ * @returns The execution result
299
+ */
300
+ @Mutation(() => ExecuteMCPToolResult)
301
+ async ExecuteMCPTool(
302
+ @Arg('input') input: ExecuteMCPToolInput,
303
+ @Ctx() ctx: AppContext
304
+ ): Promise<ExecuteMCPToolResult> {
305
+ const user = ctx.userPayload.userRecord;
306
+ if (!user) {
307
+ return {
308
+ Success: false,
309
+ ErrorMessage: 'User is not authenticated'
310
+ };
311
+ }
312
+
313
+ const { ConnectionID, ToolID, ToolName, InputArgs } = input;
314
+ const startTime = Date.now();
315
+
316
+ try {
317
+ // Check API key scope authorization
318
+ LogStatus(`MCPResolver: [${ToolName}] Step 1 - Checking API key authorization...`);
319
+ await this.CheckAPIKeyScopeAuthorization('mcp:execute', ConnectionID, ctx.userPayload);
320
+ LogStatus(`MCPResolver: [${ToolName}] Step 1 complete - Authorization passed (${Date.now() - startTime}ms)`);
321
+
322
+ // Get the MCP client manager instance and ensure it's initialized
323
+ LogStatus(`MCPResolver: [${ToolName}] Step 2 - Initializing MCP client manager...`);
324
+ const manager = MCPClientManager.Instance;
325
+ await manager.initialize(user);
326
+ LogStatus(`MCPResolver: [${ToolName}] Step 2 complete - Manager initialized (${Date.now() - startTime}ms)`);
327
+
328
+ // Connect if not already connected
329
+ const isConnected = manager.isConnected(ConnectionID);
330
+ LogStatus(`MCPResolver: [${ToolName}] Step 3 - Connection status: ${isConnected ? 'already connected' : 'needs connection'}`);
331
+ if (!isConnected) {
332
+ LogStatus(`MCPResolver: [${ToolName}] Connecting to MCP server for connection ${ConnectionID}...`);
333
+ try {
334
+ await manager.connect(ConnectionID, { contextUser: user });
335
+ LogStatus(`MCPResolver: [${ToolName}] Step 3 complete - Connected (${Date.now() - startTime}ms)`);
336
+ } catch (connectError) {
337
+ const connectErrorMsg = connectError instanceof Error ? connectError.message : String(connectError);
338
+ LogError(`MCPResolver: [${ToolName}] Connection failed: ${connectErrorMsg}`);
339
+ return {
340
+ Success: false,
341
+ ErrorMessage: `Failed to connect to MCP server: ${connectErrorMsg}`
342
+ };
343
+ }
344
+ }
345
+
346
+ // Parse input arguments
347
+ LogStatus(`MCPResolver: [${ToolName}] Step 4 - Parsing input arguments...`);
348
+ let parsedArgs: Record<string, unknown> = {};
349
+ if (InputArgs) {
350
+ try {
351
+ parsedArgs = JSON.parse(InputArgs);
352
+ LogStatus(`MCPResolver: [${ToolName}] Parsed args: ${JSON.stringify(parsedArgs).substring(0, 200)}...`);
353
+ } catch (parseError) {
354
+ LogError(`MCPResolver: [${ToolName}] Failed to parse InputArgs: ${parseError}`);
355
+ return {
356
+ Success: false,
357
+ ErrorMessage: 'Invalid JSON in InputArgs'
358
+ };
359
+ }
360
+ }
361
+ LogStatus(`MCPResolver: [${ToolName}] Step 4 complete - Args parsed (${Date.now() - startTime}ms)`);
362
+
363
+ // Call the tool
364
+ LogStatus(`MCPResolver: [${ToolName}] Step 5 - Calling tool on connection ${ConnectionID}...`);
365
+ LogStatus(`MCPResolver: [${ToolName}] Tool ID: ${ToolID}`);
366
+ const result: MCPToolCallResult = await manager.callTool(
367
+ ConnectionID,
368
+ ToolName,
369
+ { arguments: parsedArgs },
370
+ { contextUser: user }
371
+ );
372
+ LogStatus(`MCPResolver: [${ToolName}] Step 5 complete - Tool call returned (${Date.now() - startTime}ms)`);
373
+
374
+ // Format the result for the response - wrap in object for GraphQLJSONObject
375
+ let formattedResult: Record<string, unknown> | null = null;
376
+ if (result.content && result.content.length > 0) {
377
+ // If there's only one text content block, try to parse as JSON object
378
+ if (result.content.length === 1 && result.content[0].type === 'text') {
379
+ const textContent = result.content[0].text;
380
+ // Try to parse as JSON object
381
+ if (textContent && (textContent.startsWith('{') || textContent.startsWith('['))) {
382
+ try {
383
+ const parsed = JSON.parse(textContent);
384
+ // Wrap arrays in an object
385
+ if (Array.isArray(parsed)) {
386
+ formattedResult = { items: parsed };
387
+ } else if (typeof parsed === 'object' && parsed !== null) {
388
+ formattedResult = parsed as Record<string, unknown>;
389
+ } else {
390
+ formattedResult = { value: parsed };
391
+ }
392
+ } catch {
393
+ // Keep as wrapped string if not valid JSON
394
+ formattedResult = { text: textContent };
395
+ }
396
+ } else {
397
+ // Wrap plain text in object
398
+ formattedResult = { text: textContent };
399
+ }
400
+ } else {
401
+ // Return all content blocks wrapped in object
402
+ formattedResult = { content: result.content };
403
+ }
404
+ }
405
+
406
+ // Use structuredContent if available (already an object)
407
+ if (result.structuredContent) {
408
+ formattedResult = result.structuredContent as Record<string, unknown>;
409
+ }
410
+
411
+ LogStatus(`MCPResolver: [${ToolName}] Step 6 complete - Result formatted (${Date.now() - startTime}ms)`);
412
+ LogStatus(`MCPResolver: [${ToolName}] Tool execution complete - Success: ${result.success}, Duration: ${result.durationMs}ms, Total time: ${Date.now() - startTime}ms`);
413
+
414
+ return {
415
+ Success: result.success,
416
+ ErrorMessage: result.error,
417
+ Result: formattedResult,
418
+ DurationMs: result.durationMs
419
+ };
420
+
421
+ } catch (error) {
422
+ const errorMsg = error instanceof Error ? error.message : String(error);
423
+ const stack = error instanceof Error ? error.stack : '';
424
+ LogError(`MCPResolver: [${ToolName}] Error after ${Date.now() - startTime}ms: ${errorMsg}`);
425
+ LogError(`MCPResolver: [${ToolName}] Stack: ${stack}`);
426
+ return {
427
+ Success: false,
428
+ ErrorMessage: errorMsg
429
+ };
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Publishes a progress update to the statusUpdates subscription
435
+ */
436
+ private publishProgress(
437
+ pubSub: PubSubEngine,
438
+ sessionId: string,
439
+ connectionId: string,
440
+ phase: SyncProgressMessage['phase'],
441
+ message: string,
442
+ result?: { added: number; updated: number; deprecated: number; total: number }
443
+ ): void {
444
+ const progressMessage: SyncProgressMessage = {
445
+ resolver: 'MCPResolver',
446
+ type: 'MCPToolSyncProgress',
447
+ status: phase === 'error' ? 'error' : 'ok',
448
+ connectionId,
449
+ phase,
450
+ message,
451
+ result
452
+ };
453
+
454
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
455
+ message: JSON.stringify(progressMessage),
456
+ sessionId
457
+ });
458
+ }
459
+
460
+ /**
461
+ * Creates an error result with default values
462
+ */
463
+ private createErrorResult(errorMessage: string): SyncMCPToolsResult {
464
+ return {
465
+ Success: false,
466
+ ErrorMessage: errorMessage,
467
+ Added: 0,
468
+ Updated: 0,
469
+ Deprecated: 0,
470
+ Total: 0
471
+ };
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Tree-shaking prevention function
477
+ */
478
+ export function LoadMCPResolver(): void {
479
+ // Ensures the resolver is not tree-shaken
480
+ }
@@ -4,6 +4,7 @@ import { AppContext } from '../types.js';
4
4
  import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
5
5
  import { z } from 'zod';
6
6
  import { GetReadOnlyProvider, GetReadWriteProvider } from '../util.js';
7
+ import { ResolverBase } from '../generic/ResolverBase.js';
7
8
 
8
9
  @ObjectType()
9
10
  export class EntityDependencyResult {
@@ -162,13 +163,16 @@ export class RecordMergeResult {
162
163
  }
163
164
 
164
165
  @Resolver(RecordMergeResult)
165
- export class RecordMergeResolver {
166
+ export class RecordMergeResolver extends ResolverBase {
166
167
  @Mutation(() => RecordMergeResult)
167
168
  async MergeRecords(
168
169
  @Arg('request', () => RecordMergeRequest) request: RecordMergeRequest,
169
170
  @Ctx() { dataSource, userPayload, providers }: AppContext,
170
171
  @PubSub() pubSub: PubSubEngine
171
172
  ) {
173
+ // Check API key scope authorization for entity merge operation
174
+ await this.CheckAPIKeyScopeAuthorization('entity:merge', request.EntityName, userPayload);
175
+
172
176
  try {
173
177
  const md = GetReadWriteProvider(providers);
174
178
  const options = {};
@@ -6,6 +6,7 @@ import { GraphQLJSONObject } from 'graphql-type-json';
6
6
  import { Metadata } from '@memberjunction/core';
7
7
  import { GetReadOnlyProvider } from '../util.js';
8
8
  import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
9
+ import { ResolverBase } from '../generic/ResolverBase.js';
9
10
 
10
11
  /**
11
12
  * Input type for batch query execution - allows running multiple queries in a single network call
@@ -141,7 +142,7 @@ export class RunQueriesWithCacheCheckOutput {
141
142
  }
142
143
 
143
144
  @Resolver()
144
- export class RunQueryResolver {
145
+ export class RunQueryResolver extends ResolverBase {
145
146
  private async findQuery(md: IMetadataProvider, QueryID: string, QueryName?: string, CategoryID?: string, CategoryPath?: string, refreshMetadataIfNotFound: boolean = false): Promise<QueryInfo | null> {
146
147
  // Filter queries based on provided criteria
147
148
  const queries = md.Queries.filter(q => {
@@ -175,7 +176,7 @@ export class RunQueryResolver {
175
176
  }
176
177
  }
177
178
  @Query(() => RunQueryResultType)
178
- async GetQueryData(@Arg('QueryID', () => String) QueryID: string,
179
+ async GetQueryData(@Arg('QueryID', () => String) QueryID: string,
179
180
  @Ctx() context: AppContext,
180
181
  @Arg('CategoryID', () => String, {nullable: true}) CategoryID?: string,
181
182
  @Arg('CategoryPath', () => String, {nullable: true}) CategoryPath?: string,
@@ -184,6 +185,9 @@ export class RunQueryResolver {
184
185
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
185
186
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
186
187
  @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string): Promise<RunQueryResultType> {
188
+ // Check API key scope authorization for query execution
189
+ await this.CheckAPIKeyScopeAuthorization('query:run', QueryID, context.userPayload);
190
+
187
191
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
188
192
  const md = provider as unknown as IMetadataProvider;
189
193
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
@@ -235,7 +239,7 @@ export class RunQueryResolver {
235
239
  }
236
240
 
237
241
  @Query(() => RunQueryResultType)
238
- async GetQueryDataByName(@Arg('QueryName', () => String) QueryName: string,
242
+ async GetQueryDataByName(@Arg('QueryName', () => String) QueryName: string,
239
243
  @Ctx() context: AppContext,
240
244
  @Arg('CategoryID', () => String, {nullable: true}) CategoryID?: string,
241
245
  @Arg('CategoryPath', () => String, {nullable: true}) CategoryPath?: string,
@@ -244,6 +248,9 @@ export class RunQueryResolver {
244
248
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
245
249
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
246
250
  @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string): Promise<RunQueryResultType> {
251
+ // Check API key scope authorization for query execution
252
+ await this.CheckAPIKeyScopeAuthorization('query:run', QueryName, context.userPayload);
253
+
247
254
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
248
255
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
249
256
  const result = await rq.RunQuery(
@@ -381,6 +388,10 @@ export class RunQueryResolver {
381
388
  @Arg('input', () => [RunQueryInput]) input: RunQueryInput[],
382
389
  @Ctx() context: AppContext
383
390
  ): Promise<RunQueryResultType[]> {
391
+ // Check API key scope authorization for batch query execution
392
+ // We check against '*' since this runs multiple queries
393
+ await this.CheckAPIKeyScopeAuthorization('query:run', '*', context.userPayload);
394
+
384
395
  const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
385
396
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
386
397
 
@@ -478,6 +489,9 @@ export class RunQueryResolver {
478
489
  @Arg('input', () => [RunQueryWithCacheCheckInput]) input: RunQueryWithCacheCheckInput[],
479
490
  @Ctx() context: AppContext
480
491
  ): Promise<RunQueriesWithCacheCheckOutput> {
492
+ // Check API key scope authorization for batch query execution
493
+ await this.CheckAPIKeyScopeAuthorization('query:run', '*', context.userPayload);
494
+
481
495
  try {
482
496
  const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
483
497
 
@@ -8,6 +8,7 @@ import { UserCache } from '@memberjunction/sqlserver-dataprovider';
8
8
  import { z } from 'zod';
9
9
  import mssql from 'mssql';
10
10
  import { GetReadOnlyProvider, GetReadWriteProvider } from '../util.js';
11
+ import { ResolverBase } from '../generic/ResolverBase.js';
11
12
 
12
13
  @ObjectType()
13
14
  export class RunReportResultType {
@@ -46,9 +47,12 @@ export class CreateReportResultType {
46
47
  }
47
48
 
48
49
  @Resolver(RunReportResultType)
49
- export class ReportResolverExtended {
50
+ export class ReportResolverExtended extends ResolverBase {
50
51
  @Query(() => RunReportResultType)
51
52
  async GetReportData(@Arg('ReportID', () => String) ReportID: string, @Ctx() context: AppContext): Promise<RunReportResultType> {
53
+ // Check API key scope authorization for report run
54
+ await this.CheckAPIKeyScopeAuthorization('report:run', ReportID, context.userPayload);
55
+
52
56
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
53
57
  const rp = new RunReport(provider as unknown as IRunReportProvider);
54
58
 
@@ -71,6 +75,9 @@ export class ReportResolverExtended {
71
75
  @Arg('ConversationDetailID', () => String) ConversationDetailID: string,
72
76
  @Ctx() { dataSource, userPayload, providers }: AppContext
73
77
  ): Promise<CreateReportResultType> {
78
+ // Check API key scope authorization for report creation (uses entity:create for Reports entity)
79
+ await this.CheckAPIKeyScopeAuthorization('entity:create', 'Reports', userPayload);
80
+
74
81
  try {
75
82
  const md = GetReadWriteProvider(providers);
76
83
 
@@ -552,6 +552,9 @@ export class RunAIAgentResolver extends ResolverBase {
552
552
  @Arg('sourceArtifactId', { nullable: true }) sourceArtifactId?: string,
553
553
  @Arg('sourceArtifactVersionId', { nullable: true }) sourceArtifactVersionId?: string
554
554
  ): Promise<AIAgentRunResult> {
555
+ // Check API key scope authorization for agent execution
556
+ await this.CheckAPIKeyScopeAuthorization('agent:execute', agentId, userPayload);
557
+
555
558
  const p = GetReadWriteProvider(providers);
556
559
  return this.executeAIAgent(
557
560
  p,
@@ -741,6 +744,9 @@ export class RunAIAgentResolver extends ResolverBase {
741
744
  @Arg('sourceArtifactId', { nullable: true }) sourceArtifactId?: string,
742
745
  @Arg('sourceArtifactVersionId', { nullable: true }) sourceArtifactVersionId?: string
743
746
  ): Promise<AIAgentRunResult> {
747
+ // Check API key scope authorization for agent execution
748
+ await this.CheckAPIKeyScopeAuthorization('agent:execute', agentId, userPayload);
749
+
744
750
  const p = GetReadWriteProvider(providers);
745
751
  const currentUser = this.GetUserFromPayload(userPayload);
746
752
 
@@ -896,7 +902,7 @@ export class RunAIAgentResolver extends ResolverBase {
896
902
  private mapDetailRoleToMessageRole(role: string): 'user' | 'assistant' | 'system' {
897
903
  const roleLower = (role || '').toLowerCase();
898
904
  if (roleLower === 'user') return 'user';
899
- if (roleLower === 'assistant' || roleLower === 'agent') return 'assistant';
905
+ if (roleLower === 'assistant' || roleLower === 'agent' || roleLower === 'ai') return 'assistant';
900
906
  if (roleLower === 'system') return 'system';
901
907
  return 'user'; // Default to user
902
908
  }
@@ -305,6 +305,9 @@ export class RunAIPromptResolver extends ResolverBase {
305
305
  @Arg('rerunFromPromptRunID', { nullable: true }) rerunFromPromptRunID?: string,
306
306
  @Arg('systemPromptOverride', { nullable: true }) systemPromptOverride?: string
307
307
  ): Promise<AIPromptRunResult> {
308
+ // Check API key scope authorization for prompt execution
309
+ await this.CheckAPIKeyScopeAuthorization('prompt:execute', promptId, userPayload);
310
+
308
311
  const p = GetReadWriteProvider(providers);
309
312
  return this.executeAIPrompt(
310
313
  p,
@@ -589,8 +592,11 @@ export class RunAIPromptResolver extends ResolverBase {
589
592
  @Arg('modelPower', { nullable: true }) modelPower?: string,
590
593
  @Arg('responseFormat', { nullable: true }) responseFormat?: string
591
594
  ): Promise<SimplePromptResult> {
595
+ // Check API key scope authorization for simple prompt execution
596
+ await this.CheckAPIKeyScopeAuthorization('prompt:execute', '*', userPayload);
597
+
592
598
  const startTime = Date.now();
593
-
599
+
594
600
  try {
595
601
  LogStatus(`=== EXECUTING SIMPLE PROMPT ===`);
596
602
 
@@ -699,6 +705,9 @@ export class RunAIPromptResolver extends ResolverBase {
699
705
  @Arg('modelSize') modelSize: string,
700
706
  @Ctx() { userPayload }: { userPayload: UserPayload }
701
707
  ): Promise<EmbedTextResult> {
708
+ // Check API key scope authorization for embedding generation
709
+ await this.CheckAPIKeyScopeAuthorization('embedding:generate', '*', userPayload);
710
+
702
711
  try {
703
712
  LogStatus(`=== GENERATING EMBEDDINGS for ${textToEmbed.length} text(s) ===`);
704
713
 
@@ -29,8 +29,11 @@ export class RunTemplateResolver extends ResolverBase {
29
29
  @Ctx() { userPayload, providers }: AppContext,
30
30
  @Arg('contextData', { nullable: true }) contextData?: string
31
31
  ): Promise<TemplateRunResult> {
32
+ // Check API key scope authorization for template execution
33
+ await this.CheckAPIKeyScopeAuthorization('template:execute', templateId, userPayload);
34
+
32
35
  const startTime = Date.now();
33
-
36
+
34
37
  try {
35
38
  LogStatus(`=== RUNNING TEMPLATE FOR ID: ${templateId} ===`);
36
39