@memberjunction/server 3.3.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.
- package/README.md +59 -0
- package/dist/auth/BaseAuthProvider.d.ts +1 -0
- package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/BaseAuthProvider.js +2 -0
- package/dist/auth/BaseAuthProvider.js.map +1 -1
- package/dist/auth/IAuthProvider.d.ts +1 -0
- package/dist/auth/IAuthProvider.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +431 -2
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3052 -379
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +1 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +30 -0
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/resolvers/APIKeyResolver.d.ts +2 -1
- package/dist/resolvers/APIKeyResolver.d.ts.map +1 -1
- package/dist/resolvers/APIKeyResolver.js +4 -1
- package/dist/resolvers/APIKeyResolver.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +2 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +4 -1
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +5 -4
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +7 -4
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts +2 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.js +3 -1
- package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +2 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +10 -3
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts +37 -0
- package/dist/resolvers/MCPResolver.d.ts.map +1 -0
- package/dist/resolvers/MCPResolver.js +363 -0
- package/dist/resolvers/MCPResolver.js.map +1 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts +2 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +3 -1
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +2 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +6 -1
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/ReportResolver.d.ts +2 -1
- package/dist/resolvers/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js +4 -1
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +2 -0
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js +3 -0
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +1 -0
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/TaskResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskResolver.js +1 -0
- package/dist/resolvers/TaskResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +4 -0
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/package.json +47 -46
- package/src/auth/BaseAuthProvider.ts +3 -0
- package/src/auth/IAuthProvider.ts +5 -0
- package/src/config.ts +2 -2
- package/src/generated/generated.ts +2020 -334
- package/src/generic/ResolverBase.ts +89 -3
- package/src/index.ts +10 -2
- package/src/resolvers/APIKeyResolver.ts +8 -1
- package/src/resolvers/ActionResolver.ts +8 -1
- package/src/resolvers/DatasetResolver.ts +11 -4
- package/src/resolvers/EntityCommunicationsResolver.ts +5 -1
- package/src/resolvers/GetDataContextDataResolver.ts +14 -6
- package/src/resolvers/MCPResolver.ts +480 -0
- package/src/resolvers/MergeRecordsResolver.ts +5 -1
- package/src/resolvers/QueryResolver.ts +17 -3
- package/src/resolvers/ReportResolver.ts +8 -1
- package/src/resolvers/RunAIAgentResolver.ts +6 -0
- package/src/resolvers/RunAIPromptResolver.ts +10 -1
- package/src/resolvers/RunTemplateResolver.ts +4 -1
- package/src/resolvers/TaskResolver.ts +3 -0
- package/src/resolvers/UserResolver.ts +15 -3
- package/dist/resolvers/AskSkipResolver.d.ts +0 -123
- package/dist/resolvers/AskSkipResolver.d.ts.map +0 -1
- package/dist/resolvers/AskSkipResolver.js +0 -1788
- package/dist/resolvers/AskSkipResolver.js.map +0 -1
- package/dist/scheduler/LearningCycleScheduler.d.ts +0 -4
- package/dist/scheduler/LearningCycleScheduler.d.ts.map +0 -1
- package/dist/scheduler/LearningCycleScheduler.js +0 -4
- package/dist/scheduler/LearningCycleScheduler.js.map +0 -1
- package/src/resolvers/AskSkipResolver.ts +0 -3446
- 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
|
|
|
@@ -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
|
|