Skip to content
Open
7 changes: 7 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,15 @@
updateActivityLogStatus: () => Promise.resolve(),
};

readonly workflowsService: ForestAdminClient['workflowsService'] = {
listMcpEnabledWorkflows: () => Promise.resolve([]),
triggerMcpWorkflow: () => Promise.resolve({ runId: '1', runState: 'loading' }),
getMcpWorkflowRun: () =>
Promise.resolve({ runState: 'loading', currentStep: null, waitingForHumanInput: false }),
};

readonly permissionService: any;

Check warning on line 72 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 73 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
45 changes: 45 additions & 0 deletions packages/agent-testing/test/forest-admin-client-mock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import ForestAdminClientMock from '../src/forest-admin-client-mock';

describe('ForestAdminClientMock', () => {
describe('workflowsService', () => {
it('should resolve an empty list of MCP-enabled workflows', async () => {
const client = new ForestAdminClientMock();

await expect(
client.workflowsService.listMcpEnabledWorkflows({
forestServerToken: 'token',
renderingId: '1',
}),
).resolves.toEqual([]);
});

it('should resolve a loading run when triggering a workflow', async () => {
const client = new ForestAdminClientMock();

await expect(
client.workflowsService.triggerMcpWorkflow({
forestServerToken: 'token',
renderingId: '1',
workflowId: 'wf-1',
recordId: '42',
}),
).resolves.toEqual({ runId: '1', runState: 'loading' });
});

it('should resolve a loading run status when fetching a workflow run', async () => {
const client = new ForestAdminClientMock();

await expect(
client.workflowsService.getMcpWorkflowRun({
forestServerToken: 'token',
renderingId: '1',
runId: '1',
}),
).resolves.toEqual({
runState: 'loading',
currentStep: null,
waitingForHumanInput: false,
});
});
});
});
1 change: 1 addition & 0 deletions packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,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
5 changes: 5 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,11 @@ const forestAdminClientFactory = ForestAdminClientFactory.define(() => ({
createMcpActivityLog: jest.fn(),
updateActivityLogStatus: jest.fn(),
},
workflowsService: {
listMcpEnabledWorkflows: jest.fn(),
triggerMcpWorkflow: jest.fn(),
getMcpWorkflowRun: 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
12 changes: 12 additions & 0 deletions packages/forestadmin-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,17 @@ export {
ActivityLogType,
CreateActivityLogParams,
UpdateActivityLogStatusParams,
McpWorkflow,
ListMcpWorkflowsParams,
TriggerMcpWorkflowParams,
GetMcpWorkflowRunParams,
WorkflowRunState,
WorkflowRunStep,
WorkflowRunStatus,
WorkflowRunTriggerResult,
// Service interfaces for MCP
ActivityLogsServiceInterface,
WorkflowsServiceInterface,
SchemaServiceInterface,
} from './types';
export { IpWhitelistConfiguration } from './ip-whitelist/types';
Expand All @@ -55,6 +64,7 @@ export default function createForestAdminClient(
ipWhitelist,
schema,
activityLogs,
workflows,
auth,
modelCustomizationService,
mcpServerConfigService,
Expand All @@ -71,6 +81,7 @@ export default function createForestAdminClient(
ipWhitelist,
schema,
activityLogs,
workflows,
auth,
modelCustomizationService,
mcpServerConfigService,
Expand All @@ -94,6 +105,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';
56 changes: 56 additions & 0 deletions packages/forestadmin-client/src/permissions/forest-http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import type {
ForestAdminServerInterface,
ForestSchemaCollection,
IpWhitelistRulesResponse,
McpWorkflow,
WorkflowRunStatus,
WorkflowRunTriggerResult,
} from '../types';
import type { HttpOptions } from '../utils/http-options';

Expand Down Expand Up @@ -151,4 +154,57 @@ 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/mcp-workflows${query}`,
bearerToken: options.bearerToken,
headers: { 'forest-rendering-id': renderingId, ...options.headers },
});
}

async triggerMcpWorkflow(
options: ActivityLogHttpOptions,
renderingId: string,
workflowId: string,
recordId: string,
): Promise<WorkflowRunTriggerResult> {
// The orchestrator returns a numeric runId; normalize it to the string form expected by
// getMcpWorkflowRun. workflowName/collectionName are passed through when present so the
// caller can label the audit log without listing every workflow first.
const result = await ServerUtils.queryWithBearerToken<
Omit<WorkflowRunTriggerResult, 'runId'> & { runId: number | string }
>({
forestServerUrl: options.forestServerUrl,
method: 'post',
path: `/api/workflow-orchestrator/mcp-workflows/${encodeURIComponent(workflowId)}/start`,
bearerToken: options.bearerToken,
body: { recordId },
headers: { 'forest-rendering-id': renderingId, ...options.headers },
});

return { ...result, runId: String(result.runId) };
}

async getMcpWorkflowRun(
options: ActivityLogHttpOptions,
renderingId: string,
runId: string,
): Promise<WorkflowRunStatus> {
return ServerUtils.queryWithBearerToken<WorkflowRunStatus>({
forestServerUrl: options.forestServerUrl,
method: 'get',
path: `/api/workflow-orchestrator/mcp-workflows/runs/${encodeURIComponent(runId)}`,
bearerToken: options.bearerToken,
headers: { 'forest-rendering-id': renderingId, ...options.headers },
});
}
}
97 changes: 96 additions & 1 deletion packages/forestadmin-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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 @@ -252,7 +253,8 @@ export type ActivityLogAction =
| 'update'
| 'delete'
| 'listRelatedData'
| 'describeCollection';
| 'describeCollection'
| 'triggerWorkflow';

export type ActivityLogType = 'read' | 'write';

Expand Down Expand Up @@ -282,6 +284,81 @@ 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;
}

/**
* The lifecycle state of a workflow run, as persisted by the orchestrator.
*/
export type WorkflowRunState = 'started' | 'pending' | 'loading' | 'aborted' | 'finished';

/**
* The outcome of starting a workflow run: the run continues asynchronously server-side.
* `runId` is normalized to a string so it can be fed back to `getMcpWorkflowRun` as-is.
* `workflowName`/`collectionName` are echoed by the start endpoint to build the audit label
* without a second round-trip; they are optional so older servers degrade gracefully.
*/
export interface WorkflowRunTriggerResult {
runId: string;
runState: WorkflowRunState;
workflowName?: string;
collectionName?: string | null;
}

export interface TriggerMcpWorkflowParams {
forestServerToken: string;
renderingId: string;
workflowId: string;
recordId: string;
}

/**
* The step a run is currently on, as derived server-side from the workflow history.
*/
export interface WorkflowRunStep {
name: string;
type: string;
}

/**
* The normalized status of a workflow run, as exposed for external (MCP) consumption.
* `result` is the terminal output when finished; `error` the failure detail otherwise.
*/
export interface WorkflowRunStatus {
runState: WorkflowRunState;
currentStep: WorkflowRunStep | null;
waitingForHumanInput: boolean;
result?: unknown;
error?: unknown;
}

export interface GetMcpWorkflowRunParams {
forestServerToken: string;
renderingId: string;
runId: string;
}

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

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

// Workflow operations
listMcpEnabledWorkflows?: (
options: ActivityLogHttpOptions,
renderingId: string,
collectionName?: string,
) => Promise<McpWorkflow[]>;
triggerMcpWorkflow?: (
options: ActivityLogHttpOptions,
renderingId: string,
workflowId: string,
recordId: string,
) => Promise<WorkflowRunTriggerResult>;
getMcpWorkflowRun?: (
options: ActivityLogHttpOptions,
renderingId: string,
runId: string,
) => Promise<WorkflowRunStatus>;
}

export type ActivityLogHttpOptions = {
Expand Down
Loading
Loading