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
2 changes: 2 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 @@ -65,10 +65,12 @@
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
16 changes: 16 additions & 0 deletions packages/agent-testing/test/forest-admin-client-mock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,21 @@ describe('ForestAdminClientMock', () => {
}),
).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/test/__factories__/forest-admin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const forestAdminClientFactory = ForestAdminClientFactory.define(() => ({
workflowsService: {
listMcpEnabledWorkflows: jest.fn(),
triggerMcpWorkflow: jest.fn(),
getMcpWorkflowRun: jest.fn(),
},
subscribeToServerEvents: jest.fn(),
close: jest.fn(),
Expand Down
3 changes: 3 additions & 0 deletions packages/forestadmin-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export {
McpWorkflow,
ListMcpWorkflowsParams,
TriggerMcpWorkflowParams,
GetMcpWorkflowRunParams,
WorkflowRunState,
WorkflowRunStep,
WorkflowRunStatus,
WorkflowRunTriggerResult,
// Service interfaces for MCP
ActivityLogsServiceInterface,
Expand Down
15 changes: 15 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,7 @@ import type {
ForestSchemaCollection,
IpWhitelistRulesResponse,
McpWorkflow,
WorkflowRunStatus,
WorkflowRunTriggerResult,
} from '../types';
import type { HttpOptions } from '../utils/http-options';
Expand Down Expand Up @@ -185,4 +186,18 @@ export default class ForestHttpApi implements ForestAdminServerInterface {
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 },
});
}
}
32 changes: 32 additions & 0 deletions packages/forestadmin-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,39 @@ export interface TriggerMcpWorkflowParams {
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>;
}

/**
Expand Down Expand Up @@ -379,6 +406,11 @@ export interface ForestAdminServerInterface {
workflowId: string,
recordId: string,
) => Promise<WorkflowRunTriggerResult>;
getMcpWorkflowRun?: (
options: ActivityLogHttpOptions,
renderingId: string,
runId: string,
) => Promise<WorkflowRunStatus>;
}

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

Expand Down Expand Up @@ -57,4 +59,22 @@ export default class WorkflowsService {
recordId,
);
}

async getMcpWorkflowRun(params: GetMcpWorkflowRunParams): Promise<WorkflowRunStatus> {
const { forestServerToken, renderingId, runId } = params;

if (!this.forestAdminServerInterface.getMcpWorkflowRun) {
throw new Error('The configured Forest server transport does not support getMcpWorkflowRun.');
}

return this.forestAdminServerInterface.getMcpWorkflowRun(
{
forestServerUrl: this.options.forestServerUrl,
bearerToken: forestServerToken,
headers: this.options.headers,
},
renderingId,
runId,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const forestAdminServerInterface = {
// Workflow operations
listMcpEnabledWorkflows: jest.fn(),
triggerMcpWorkflow: jest.fn(),
getMcpWorkflowRun: jest.fn(),
}),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,52 @@ describe('ForestHttpApi', () => {
);
});
});

describe('getMcpWorkflowRun', () => {
it('should GET the workflow run endpoint with the rendering id header', async () => {
const runStatus = {
runState: 'finished',
currentStep: null,
waitingForHumanInput: false,
result: { ok: true },
};
(ServerUtils.queryWithBearerToken as jest.Mock).mockResolvedValue(runStatus);

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

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

it('should url-encode the run id in the path', async () => {
(ServerUtils.queryWithBearerToken as jest.Mock).mockResolvedValue({
runState: 'started',
currentStep: null,
waitingForHumanInput: false,
});

await new ForestHttpApi().getMcpWorkflowRun(
{ forestServerUrl: options.forestServerUrl, bearerToken: 'bearer-token' },
'12345',
'run/with space',
);

expect(ServerUtils.queryWithBearerToken).toHaveBeenCalledWith(
expect.objectContaining({
method: 'get',
path: '/api/workflow-orchestrator/mcp-workflows/runs/run%2Fwith%20space',
}),
);
});
});
});
65 changes: 65 additions & 0 deletions packages/forestadmin-client/test/workflows/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,69 @@ describe('WorkflowsService', () => {
).rejects.toThrow('does not support triggerMcpWorkflow');
});
});

describe('getMcpWorkflowRun', () => {
const runStatus = {
runState: 'finished' as const,
currentStep: null,
waitingForHumanInput: false,
result: { ok: true },
};

it('should forward the identity and runId to the transport and return the status', async () => {
mockForestAdminServerInterface.getMcpWorkflowRun.mockResolvedValue(runStatus);

const service = new WorkflowsService(mockForestAdminServerInterface, options);
const result = await service.getMcpWorkflowRun({
forestServerToken: 'test-token',
renderingId: '12345',
runId: '7',
});

expect(result).toEqual(runStatus);
expect(mockForestAdminServerInterface.getMcpWorkflowRun).toHaveBeenCalledWith(
{ forestServerUrl: options.forestServerUrl, bearerToken: 'test-token', headers: undefined },
'12345',
'7',
);
});

it('should pass custom headers when provided', async () => {
mockForestAdminServerInterface.getMcpWorkflowRun.mockResolvedValue(runStatus);

const service = new WorkflowsService(mockForestAdminServerInterface, {
...options,
headers: { 'Forest-Application-Source': 'MCP' },
});
await service.getMcpWorkflowRun({
forestServerToken: 'test-token',
renderingId: '12345',
runId: '7',
});

expect(mockForestAdminServerInterface.getMcpWorkflowRun).toHaveBeenCalledWith(
expect.objectContaining({
bearerToken: 'test-token',
headers: { 'Forest-Application-Source': 'MCP' },
}),
'12345',
'7',
);
});

it('should throw when the transport does not implement getMcpWorkflowRun', async () => {
delete (mockForestAdminServerInterface as Partial<ForestAdminServerInterface>)
.getMcpWorkflowRun;

const service = new WorkflowsService(mockForestAdminServerInterface, options);

await expect(
service.getMcpWorkflowRun({
forestServerToken: 'test-token',
renderingId: '12345',
runId: '7',
}),
).rejects.toThrow('does not support getMcpWorkflowRun');
});
});
});
2 changes: 2 additions & 0 deletions packages/mcp-server/src/http-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ export type {
ActivityLogType,
CreateActivityLogParams,
ForestServerClient,
GetMcpWorkflowRunParams,
ListMcpWorkflowsParams,
McpWorkflow,
TriggerMcpWorkflowParams,
WorkflowRunStatus,
WorkflowRunTriggerResult,
UpdateActivityLogStatusParams,
ForestSchemaCollection,
Expand Down
6 changes: 6 additions & 0 deletions packages/mcp-server/src/http-client/mcp-http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import type {
CreateActivityLogParams,
ForestSchemaCollection,
ForestServerClient,
GetMcpWorkflowRunParams,
ListMcpWorkflowsParams,
McpWorkflow,
SchemaServiceInterface,
TriggerMcpWorkflowParams,
UpdateActivityLogStatusParams,
WorkflowRunStatus,
WorkflowRunTriggerResult,
WorkflowsServiceInterface,
} from './types';
Expand Down Expand Up @@ -48,4 +50,8 @@ export default class ForestServerClientImpl implements ForestServerClient {
async triggerWorkflow(params: TriggerMcpWorkflowParams): Promise<WorkflowRunTriggerResult> {
return this.workflowsService.triggerMcpWorkflow(params);
}

async getWorkflowRun(params: GetMcpWorkflowRunParams): Promise<WorkflowRunStatus> {
return this.workflowsService.getMcpWorkflowRun(params);
}
}
9 changes: 9 additions & 0 deletions packages/mcp-server/src/http-client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import type {
ForestSchemaAction,
ForestSchemaCollection,
ForestSchemaField,
GetMcpWorkflowRunParams,
ListMcpWorkflowsParams,
McpWorkflow,
SchemaServiceInterface,
TriggerMcpWorkflowParams,
UpdateActivityLogStatusParams,
WorkflowRunStatus,
WorkflowRunTriggerResult,
WorkflowsServiceInterface,
} from '@forestadmin/forestadmin-client';
Expand All @@ -26,11 +28,13 @@ export type {
ForestSchemaAction,
ForestSchemaCollection,
ForestSchemaField,
GetMcpWorkflowRunParams,
ListMcpWorkflowsParams,
McpWorkflow,
SchemaServiceInterface,
TriggerMcpWorkflowParams,
UpdateActivityLogStatusParams,
WorkflowRunStatus,
WorkflowRunTriggerResult,
WorkflowsServiceInterface,
};
Expand Down Expand Up @@ -74,4 +78,9 @@ export interface ForestServerClient {
* Starts a run of an MCP-enabled workflow on a record and returns its runId (async).
*/
triggerWorkflow(params: TriggerMcpWorkflowParams): Promise<WorkflowRunTriggerResult>;

/**
* Reads the normalized status of a workflow run, scoped to the caller.
*/
getWorkflowRun(params: GetMcpWorkflowRunParams): Promise<WorkflowRunStatus>;
}
Loading
Loading