Skip to content
Open
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
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 @@ -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
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';
49 changes: 49 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,9 @@ import type {
ForestAdminServerInterface,
ForestSchemaCollection,
IpWhitelistRulesResponse,
McpWorkflow,
WorkflowRunStatus,
WorkflowRunTriggerResult,
} from '../types';
import type { HttpOptions } from '../utils/http-options';
import type { ToolConfig } from '@forestadmin/ai-proxy';
Expand Down Expand Up @@ -151,4 +154,50 @@ 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> {
return ServerUtils.queryWithBearerToken<WorkflowRunTriggerResult>({
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 },
});
}

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 },
});
}
}
92 changes: 91 additions & 1 deletion 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 @@ -253,7 +254,8 @@ export type ActivityLogAction =
| 'update'
| 'delete'
| 'listRelatedData'
| 'describeCollection';
| 'describeCollection'
| 'triggerWorkflow';

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

Expand Down Expand Up @@ -283,6 +285,76 @@ 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.
*/
export interface WorkflowRunTriggerResult {
runId: number;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/types.ts:312

WorkflowRunTriggerResult.runId is typed as number, but GetMcpWorkflowRunParams.runId requires a string, so the result of triggerMcpWorkflow cannot be passed directly into getMcpWorkflowRun without a manual conversion. The mismatch also means MCP tool input validation (string-only) rejects the numeric ID. Use one consistent type for runId across both interfaces.

Suggested change
runId: number;
runId: string;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/forestadmin-client/src/types.ts around line 312:

`WorkflowRunTriggerResult.runId` is typed as `number`, but `GetMcpWorkflowRunParams.runId` requires a `string`, so the result of `triggerMcpWorkflow` cannot be passed directly into `getMcpWorkflowRun` without a manual conversion. The mismatch also means MCP tool input validation (string-only) rejects the numeric ID. Use one consistent type for `runId` across both interfaces.

runState: WorkflowRunState;
}

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 @@ -321,6 +393,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