Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/agent-testing/src/forest-admin-client-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
export default class ForestAdminClientMock implements ForestAdminClient {
readonly chartHandler: ChartHandlerInterface;
readonly contextVariablesInstantiator: ContextVariablesInstantiatorInterface = {
buildContextVariables: () => ({} as any), // TODO: return actual context variables

Check warning on line 47 in packages/agent-testing/src/forest-admin-client-mock.ts

View workflow job for this annotation

GitHub Actions / Linting & Testing (agent-testing)

Unexpected any. Specify a different type
};

readonly modelCustomizationService: ModelCustomizationService;
Expand All @@ -62,8 +62,12 @@
updateActivityLogStatus: () => Promise.resolve(),
};

readonly workflowsService: ForestAdminClient['workflowsService'] = {
listMcpEnabledWorkflows: () => Promise.resolve([]),
};

readonly permissionService: any;

Check warning on line 69 in packages/agent-testing/src/forest-admin-client-mock.ts

View workflow job for this annotation

GitHub Actions / Linting & Testing (agent-testing)

Unexpected any. Specify a different type
readonly authService: any;

Check warning on line 70 in packages/agent-testing/src/forest-admin-client-mock.ts

View workflow job for this annotation

GitHub Actions / Linting & Testing (agent-testing)

Unexpected any. Specify a different type

constructor() {
this.permissionService = {
Expand Down
1 change: 1 addition & 0 deletions packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
const forestServerClient = new ForestServerClientImpl(
this.options.forestAdminClient.schemaService,
this.options.forestAdminClient.activityLogsService,
this.options.forestAdminClient.workflowsService,
this.options.forestServerUrl,
);

Expand Down
3 changes: 3 additions & 0 deletions packages/agent/test/__factories__/forest-admin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ const forestAdminClientFactory = ForestAdminClientFactory.define(() => ({
createMcpActivityLog: jest.fn(),
updateActivityLogStatus: jest.fn(),
},
workflowsService: {
listMcpEnabledWorkflows: jest.fn(),
},
subscribeToServerEvents: jest.fn(),
close: jest.fn(),
onRefreshCustomizations: jest.fn(),
Expand Down
3 changes: 3 additions & 0 deletions packages/forestadmin-client/src/build-application-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import UserPermissionService from './permissions/user-permission';
import SchemaService from './schema';
import ContextVariablesInstantiator from './utils/context-variables-instantiator';
import defaultLogger from './utils/default-logger';
import WorkflowsService from './workflows';

export default function buildApplicationServices(
forestAdminServerInterface: ForestAdminServerInterface,
Expand All @@ -31,6 +32,7 @@ export default function buildApplicationServices(
renderingPermission: RenderingPermissionService;
schema: SchemaService;
activityLogs: ActivityLogsService;
workflows: WorkflowsService;
contextVariables: ContextVariablesInstantiator;
ipWhitelist: IpWhiteListService;
permission: PermissionService;
Expand Down Expand Up @@ -89,6 +91,7 @@ export default function buildApplicationServices(
ipWhitelist: new IpWhiteListService(forestAdminServerInterface, optionsWithDefaults),
schema: new SchemaService(forestAdminServerInterface, optionsWithDefaults),
activityLogs: new ActivityLogsService(forestAdminServerInterface, optionsWithDefaults),
workflows: new WorkflowsService(forestAdminServerInterface, optionsWithDefaults),
auth: forestAdminServerInterface.makeAuthService(optionsWithDefaults),
modelCustomizationService: new ModelCustomizationFromApiService(
forestAdminServerInterface,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
PermissionService,
} from './types';
import type ContextVariablesInstantiator from './utils/context-variables-instantiator';
import type WorkflowsService from './workflows';

import verifyAndExtractApproval from './permissions/verify-approval';

Expand All @@ -32,6 +33,7 @@ export default class ForestAdminClientWithCache implements ForestAdminClient {
protected readonly ipWhitelistService: IpWhiteListService,
public readonly schemaService: SchemaService,
public readonly activityLogsService: ActivityLogsService,
public readonly workflowsService: WorkflowsService,
public readonly authService: ForestAdminAuthServiceInterface,
public readonly modelCustomizationService: ModelCustomizationService,
public readonly mcpServerConfigService: McpServerConfigService,
Expand Down
6 changes: 6 additions & 0 deletions packages/forestadmin-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ export {
ActivityLogType,
CreateActivityLogParams,
UpdateActivityLogStatusParams,
McpWorkflow,
ListMcpWorkflowsParams,
// Service interfaces for MCP
ActivityLogsServiceInterface,
WorkflowsServiceInterface,
SchemaServiceInterface,
} from './types';
export { IpWhitelistConfiguration } from './ip-whitelist/types';
Expand All @@ -55,6 +58,7 @@ export default function createForestAdminClient(
ipWhitelist,
schema,
activityLogs,
workflows,
auth,
modelCustomizationService,
mcpServerConfigService,
Expand All @@ -71,6 +75,7 @@ export default function createForestAdminClient(
ipWhitelist,
schema,
activityLogs,
workflows,
auth,
modelCustomizationService,
mcpServerConfigService,
Expand All @@ -94,6 +99,7 @@ export { default as ServerUtils } from './utils/server';
// export is necessary for the agent-generator package
export { default as SchemaService, SchemaServiceOptions } from './schema';
export { default as ActivityLogsService, ActivityLogsOptions } from './activity-logs';
export { default as WorkflowsService, WorkflowsServiceOptions } from './workflows';

export * from './auth/errors';
export * from './utils/errors';
17 changes: 17 additions & 0 deletions packages/forestadmin-client/src/permissions/forest-http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ForestAdminServerInterface,
ForestSchemaCollection,
IpWhitelistRulesResponse,
McpWorkflow,
} from '../types';
import type { HttpOptions } from '../utils/http-options';
import type { ToolConfig } from '@forestadmin/ai-proxy';
Expand Down Expand Up @@ -151,4 +152,20 @@ export default class ForestHttpApi implements ForestAdminServerInterface {
headers: options.headers,
});
}

async listMcpEnabledWorkflows(
options: ActivityLogHttpOptions,
renderingId: string,
collectionName?: string,
): Promise<McpWorkflow[]> {
const query = collectionName ? `?collectionName=${encodeURIComponent(collectionName)}` : '';

return ServerUtils.queryWithBearerToken<McpWorkflow[]>({
forestServerUrl: options.forestServerUrl,
method: 'get',
path: `/api/workflow-orchestrator/workflows${query}`,
bearerToken: options.bearerToken,
headers: { 'forest-rendering-id': renderingId, ...options.headers },
});
}
}
30 changes: 30 additions & 0 deletions packages/forestadmin-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface ForestAdminClient {
readonly authService: ForestAdminAuthServiceInterface;
readonly schemaService: SchemaServiceInterface;
readonly activityLogsService: ActivityLogsServiceInterface;
readonly workflowsService: WorkflowsServiceInterface;

verifySignedActionParameters<TSignedParameters>(signedParameters: string): TSignedParameters;

Expand Down Expand Up @@ -283,6 +284,28 @@ export interface ActivityLogsServiceInterface {
updateActivityLogStatus: (params: UpdateActivityLogStatusParams) => Promise<void>;
}

/**
* An MCP-enabled workflow, as returned by the Forest server's workflow listing endpoint.
*/
export interface McpWorkflow {
workflowId: string;
name: string;
collectionName: string | null;
}

export interface ListMcpWorkflowsParams {
forestServerToken: string;
renderingId: string;
collectionName?: string;
}

/**
* Service interface for workflow operations (MCP-related).
*/
export interface WorkflowsServiceInterface {
listMcpEnabledWorkflows: (params: ListMcpWorkflowsParams) => Promise<McpWorkflow[]>;
}

/**
* Service interface for schema operations (extended for MCP).
*/
Expand Down Expand Up @@ -321,6 +344,13 @@ export interface ForestAdminServerInterface {
id: string,
body: object,
) => Promise<void>;

// Workflow operations
listMcpEnabledWorkflows?: (
options: ActivityLogHttpOptions,
renderingId: string,
collectionName?: string,
) => Promise<McpWorkflow[]>;
}

export type ActivityLogHttpOptions = {
Expand Down
27 changes: 27 additions & 0 deletions packages/forestadmin-client/src/workflows/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ForestAdminServerInterface, ListMcpWorkflowsParams, McpWorkflow } from '../types';

export type WorkflowsServiceOptions = {
forestServerUrl: string;
headers?: Record<string, string>;
};

export default class WorkflowsService {
constructor(
private forestAdminServerInterface: ForestAdminServerInterface,
private options: WorkflowsServiceOptions,
) {}

async listMcpEnabledWorkflows(params: ListMcpWorkflowsParams): Promise<McpWorkflow[]> {
const { forestServerToken, renderingId, collectionName } = params;

return this.forestAdminServerInterface.listMcpEnabledWorkflows(
{
forestServerUrl: this.options.forestServerUrl,
bearerToken: forestServerToken,
headers: this.options.headers,
},
renderingId,
collectionName,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import permissionServiceFactory from './permissions/permission';
import renderingPermissionsFactory from './permissions/rendering-permission';
import schemaServiceFactory from './schema';
import contextVariablesInstantiatorFactory from './utils/context-variables-instantiator';
import workflowsServiceFactory from './workflows';
import ForestAdminClient from '../../src/forest-admin-client-with-cache';

export class ForestAdminClientFactory extends Factory<ForestAdminClient> {
Expand All @@ -36,6 +37,7 @@ const forestAdminClientFactory = ForestAdminClientFactory.define(
ipWhitelistServiceFactory.build(),
schemaServiceFactory.build(),
activityLogsServiceFactory.build(),
workflowsServiceFactory.build(),
authServiceFactory.build(),
modelCustomizationServiceFactory.build(),
mcpServerConfigServiceFactory.build(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const forestAdminServerInterface = {
createActivityLog: jest.fn(),
createMcpActivityLog: jest.fn(),
updateActivityLogStatus: jest.fn(),
// Workflow operations
listMcpEnabledWorkflows: jest.fn(),
}),
};

Expand Down
1 change: 1 addition & 0 deletions packages/forestadmin-client/test/__factories__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { default as forestAdminClientOptions } from './forest-admin-client-optio
export { default as ipWhiteList } from './ip-whitelist';
export { default as schema } from './schema';
export { default as activityLogs } from './activity-logs';
export { default as workflows } from './workflows';
export { default as auth } from './auth';
export { default as modelCustomization } from './model-customizations/model-customization-from-api';
export { default as mcpServerConfig } from './mcp-server-config';
11 changes: 11 additions & 0 deletions packages/forestadmin-client/test/__factories__/workflows/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Factory } from 'fishery';

import WorkflowsService from '../../../src/workflows';
import forestAdminClientOptions from '../forest-admin-client-options';
import forestAdminServerInterface from '../forest-admin-server-interface';

const workflowsServiceFactory = Factory.define<WorkflowsService>(() => {
return new WorkflowsService(forestAdminServerInterface.build(), forestAdminClientOptions.build());
});

export default workflowsServiceFactory;
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('ForestAdminClientWithCache', () => {
whiteListService,
factories.schema.build(),
factories.activityLogs.build(),
factories.workflows.build(),
factories.auth.build(),
factories.modelCustomization.build(),
factories.mcpServerConfig.build(),
Expand Down Expand Up @@ -53,6 +54,7 @@ describe('ForestAdminClientWithCache', () => {
factories.ipWhiteList.build(),
schemaService,
factories.activityLogs.build(),
factories.workflows.build(),
factories.auth.build(),
factories.modelCustomization.build(),
factories.mcpServerConfig.build(),
Expand Down Expand Up @@ -88,6 +90,7 @@ describe('ForestAdminClientWithCache', () => {
factories.ipWhiteList.build(),
factories.schema.build(),
factories.activityLogs.build(),
factories.workflows.build(),
factories.auth.build(),
factories.modelCustomization.build(),
factories.mcpServerConfig.build(),
Expand Down Expand Up @@ -117,6 +120,7 @@ describe('ForestAdminClientWithCache', () => {
factories.ipWhiteList.build(),
factories.schema.build(),
factories.activityLogs.build(),
factories.workflows.build(),
factories.auth.build(),
factories.modelCustomization.build(),
factories.mcpServerConfig.build(),
Expand All @@ -142,6 +146,7 @@ describe('ForestAdminClientWithCache', () => {
factories.ipWhiteList.build(),
factories.schema.build(),
factories.activityLogs.build(),
factories.workflows.build(),
factories.auth.build(),
factories.modelCustomization.build(),
factories.mcpServerConfig.build(),
Expand All @@ -168,6 +173,7 @@ describe('ForestAdminClientWithCache', () => {
factories.ipWhiteList.build(),
factories.schema.build(),
factories.activityLogs.build(),
factories.workflows.build(),
factories.auth.build(),
factories.modelCustomization.build(),
factories.mcpServerConfig.build(),
Expand Down Expand Up @@ -204,6 +210,7 @@ describe('ForestAdminClientWithCache', () => {
factories.ipWhiteList.build(),
factories.schema.build(),
factories.activityLogs.build(),
factories.workflows.build(),
factories.auth.build(),
factories.modelCustomization.build(),
factories.mcpServerConfig.build(),
Expand All @@ -229,6 +236,7 @@ describe('ForestAdminClientWithCache', () => {
factories.ipWhiteList.build(),
factories.schema.build(),
factories.activityLogs.build(),
factories.workflows.build(),
factories.auth.build(),
factories.modelCustomization.build(),
factories.mcpServerConfig.build(),
Expand All @@ -254,6 +262,7 @@ describe('ForestAdminClientWithCache', () => {
factories.ipWhiteList.build(),
factories.schema.build(),
factories.activityLogs.build(),
factories.workflows.build(),
factories.auth.build(),
factories.modelCustomization.build(),
factories.mcpServerConfig.build(),
Expand Down Expand Up @@ -281,6 +290,7 @@ describe('ForestAdminClientWithCache', () => {
factories.ipWhiteList.build(),
factories.schema.build(),
factories.activityLogs.build(),
factories.workflows.build(),
factories.auth.build(),
factories.modelCustomization.build(),
factories.mcpServerConfig.build(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,43 @@ describe('ForestHttpApi', () => {
});
});
});

describe('listMcpEnabledWorkflows', () => {
it('should GET the workflows endpoint with the rendering id header', async () => {
const workflows = [{ workflowId: 'wf-1', name: 'Refund order', collectionName: 'orders' }];
(ServerUtils.queryWithBearerToken as jest.Mock).mockResolvedValue(workflows);

const result = await new ForestHttpApi().listMcpEnabledWorkflows(
{ forestServerUrl: options.forestServerUrl, bearerToken: 'bearer-token' },
'12345',
);

expect(ServerUtils.queryWithBearerToken).toHaveBeenCalledWith({
forestServerUrl: options.forestServerUrl,
method: 'get',
path: '/api/workflow-orchestrator/workflows',
bearerToken: 'bearer-token',
headers: { 'forest-rendering-id': '12345' },
});
expect(result).toEqual(workflows);
});

it('should append the collectionName filter as a url-encoded query param', async () => {
(ServerUtils.queryWithBearerToken as jest.Mock).mockResolvedValue([]);

await new ForestHttpApi().listMcpEnabledWorkflows(
{ forestServerUrl: options.forestServerUrl, bearerToken: 'bearer-token' },
'12345',
'sales orders',
);

expect(ServerUtils.queryWithBearerToken).toHaveBeenCalledWith(
expect.objectContaining({
method: 'get',
path: '/api/workflow-orchestrator/workflows?collectionName=sales%20orders',
headers: { 'forest-rendering-id': '12345' },
}),
);
});
});
});
Loading
Loading