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
3 changes: 3 additions & 0 deletions packages/forestadmin-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export {
UpdateActivityLogStatusParams,
McpWorkflow,
ListMcpWorkflowsParams,
TriggerMcpWorkflowParams,
WorkflowRunState,
WorkflowRunTriggerResult,
// Service interfaces for MCP
ActivityLogsServiceInterface,
WorkflowsServiceInterface,
Expand Down
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 @@ -9,6 +9,7 @@ import type {
ForestSchemaCollection,
IpWhitelistRulesResponse,
McpWorkflow,
WorkflowRunTriggerResult,
} from '../types';
import type { HttpOptions } from '../utils/http-options';
import type { ToolConfig } from '@forestadmin/ai-proxy';
Expand Down Expand Up @@ -168,4 +169,20 @@ export default class ForestHttpApi implements ForestAdminServerInterface {
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/workflows/${encodeURIComponent(workflowId)}/start`,
bearerToken: options.bearerToken,
body: { recordId },
headers: { 'forest-rendering-id': renderingId, ...options.headers },
});
}
}
30 changes: 29 additions & 1 deletion packages/forestadmin-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ export type ActivityLogAction =
| 'update'
| 'delete'
| 'listRelatedData'
| 'describeCollection';
| 'describeCollection'
| 'triggerWorkflow';

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

Expand Down Expand Up @@ -299,11 +300,32 @@ export interface ListMcpWorkflowsParams {
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;
runState: WorkflowRunState;
}

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

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

/**
Expand Down Expand Up @@ -351,6 +373,12 @@ export interface ForestAdminServerInterface {
renderingId: string,
collectionName?: string,
) => Promise<McpWorkflow[]>;
triggerMcpWorkflow?: (
options: ActivityLogHttpOptions,
renderingId: string,
workflowId: string,
recordId: string,
) => Promise<WorkflowRunTriggerResult>;
}

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

export type WorkflowsServiceOptions = {
forestServerUrl: string;
Expand All @@ -24,4 +30,19 @@ export default class WorkflowsService {
collectionName,
);
}

async triggerMcpWorkflow(params: TriggerMcpWorkflowParams): Promise<WorkflowRunTriggerResult> {
const { forestServerToken, renderingId, workflowId, recordId } = params;

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

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

describe('triggerMcpWorkflow', () => {
it('should POST the record id to the workflow start endpoint with the rendering id header', async () => {
const run = { runId: 7, runState: 'loading' };
(ServerUtils.queryWithBearerToken as jest.Mock).mockResolvedValue(run);

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

expect(ServerUtils.queryWithBearerToken).toHaveBeenCalledWith({
forestServerUrl: options.forestServerUrl,
method: 'post',
path: '/api/workflow-orchestrator/workflows/wf-1/start',
bearerToken: 'bearer-token',
body: { recordId: '42' },
headers: { 'forest-rendering-id': '12345' },
});
expect(result).toEqual(run);
});

it('should url-encode the workflow id in the path', async () => {
(ServerUtils.queryWithBearerToken as jest.Mock).mockResolvedValue({
runId: 1,
runState: 'loading',
});

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

expect(ServerUtils.queryWithBearerToken).toHaveBeenCalledWith(
expect.objectContaining({
method: 'post',
path: '/api/workflow-orchestrator/workflows/wf%2Fwith%20space/start',
}),
);
});
});
});
53 changes: 53 additions & 0 deletions packages/forestadmin-client/test/workflows/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,57 @@ describe('WorkflowsService', () => {
);
});
});

describe('triggerMcpWorkflow', () => {
it('should forward the identity, workflowId and recordId to the transport and return the run', async () => {
mockForestAdminServerInterface.triggerMcpWorkflow.mockResolvedValue({
runId: 7,
runState: 'loading',
});

const service = new WorkflowsService(mockForestAdminServerInterface, options);
const result = await service.triggerMcpWorkflow({
forestServerToken: 'test-token',
renderingId: '12345',
workflowId: 'wf-1',
recordId: '42',
});

expect(result).toEqual({ runId: 7, runState: 'loading' });
expect(mockForestAdminServerInterface.triggerMcpWorkflow).toHaveBeenCalledWith(
{ forestServerUrl: options.forestServerUrl, bearerToken: 'test-token', headers: undefined },
'12345',
'wf-1',
'42',
);
});

it('should pass custom headers when provided', async () => {
mockForestAdminServerInterface.triggerMcpWorkflow.mockResolvedValue({
runId: 7,
runState: 'loading',
});

const service = new WorkflowsService(mockForestAdminServerInterface, {
...options,
headers: { 'Forest-Application-Source': 'MCP' },
});
await service.triggerMcpWorkflow({
forestServerToken: 'test-token',
renderingId: '12345',
workflowId: 'wf-1',
recordId: '42',
});

expect(mockForestAdminServerInterface.triggerMcpWorkflow).toHaveBeenCalledWith(
expect.objectContaining({
bearerToken: 'test-token',
headers: { 'Forest-Application-Source': 'MCP' },
}),
'12345',
'wf-1',
'42',
);
});
});
});
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 @@ -55,6 +55,8 @@ export type {
ForestServerClient,
ListMcpWorkflowsParams,
McpWorkflow,
TriggerMcpWorkflowParams,
WorkflowRunTriggerResult,
UpdateActivityLogStatusParams,
ForestSchemaCollection,
ForestSchemaField,
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 @@ -7,7 +7,9 @@ import type {
ListMcpWorkflowsParams,
McpWorkflow,
SchemaServiceInterface,
TriggerMcpWorkflowParams,
UpdateActivityLogStatusParams,
WorkflowRunTriggerResult,
WorkflowsServiceInterface,
} from './types';

Expand Down Expand Up @@ -42,4 +44,8 @@ export default class ForestServerClientImpl implements ForestServerClient {
async listMcpWorkflows(params: ListMcpWorkflowsParams): Promise<McpWorkflow[]> {
return this.workflowsService.listMcpEnabledWorkflows(params);
}

async triggerWorkflow(params: TriggerMcpWorkflowParams): Promise<WorkflowRunTriggerResult> {
return this.workflowsService.triggerMcpWorkflow(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 @@ -10,7 +10,9 @@ import type {
ListMcpWorkflowsParams,
McpWorkflow,
SchemaServiceInterface,
TriggerMcpWorkflowParams,
UpdateActivityLogStatusParams,
WorkflowRunTriggerResult,
WorkflowsServiceInterface,
} from '@forestadmin/forestadmin-client';

Expand All @@ -27,7 +29,9 @@ export type {
ListMcpWorkflowsParams,
McpWorkflow,
SchemaServiceInterface,
TriggerMcpWorkflowParams,
UpdateActivityLogStatusParams,
WorkflowRunTriggerResult,
WorkflowsServiceInterface,
};

Expand Down Expand Up @@ -65,4 +69,9 @@ export interface ForestServerClient {
* Lists the MCP-enabled workflows the caller can access in a rendering.
*/
listMcpWorkflows(params: ListMcpWorkflowsParams): Promise<McpWorkflow[]>;

/**
* Starts a run of an MCP-enabled workflow on a record and returns its runId (async).
*/
triggerWorkflow(params: TriggerMcpWorkflowParams): Promise<WorkflowRunTriggerResult>;
}
7 changes: 6 additions & 1 deletion packages/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import declareGetActionFormTool from './tools/get-action-form';
import declareListTool from './tools/list';
import declareListRelatedTool from './tools/list-related';
import declareListWorkflowsTool from './tools/list-workflows';
import declareTriggerWorkflowTool from './tools/trigger-workflow';
import declareUpdateTool from './tools/update';
import normalizeAgentUrl from './utils/normalize-agent-url';
import { fetchForestSchema, getCollectionNames } from './utils/schema-fetcher';
Expand Down Expand Up @@ -91,6 +92,7 @@ const SAFE_ARGUMENTS_FOR_LOGGING: Record<string, string[]> = {
associate: ['collectionName', 'relationName', 'parentRecordId', 'targetRecordId'],
dissociate: ['collectionName', 'relationName', 'parentRecordId', 'targetRecordIds'],
listWorkflows: ['collectionName'],
triggerWorkflow: ['workflowId', 'recordId'],
};

export type ToolName =
Expand All @@ -104,7 +106,8 @@ export type ToolName =
| 'dissociate'
| 'getActionForm'
| 'executeAction'
| 'listWorkflows';
| 'listWorkflows'
| 'triggerWorkflow';

/**
* Options for configuring the Forest Admin MCP Server
Expand Down Expand Up @@ -227,6 +230,7 @@ export default class ForestMCPServer {
{ name: 'getActionForm', register: () => declareGetActionFormTool(mcpServer, ctx) },
{ name: 'executeAction', register: () => declareExecuteActionTool(mcpServer, ctx) },
{ name: 'listWorkflows', register: () => declareListWorkflowsTool(mcpServer, ctx) },
{ name: 'triggerWorkflow', register: () => declareTriggerWorkflowTool(mcpServer, ctx) },
];

const enabledToolEntries = allTools.filter(tool => this.enabledTools.has(tool.name));
Expand Down Expand Up @@ -265,6 +269,7 @@ export default class ForestMCPServer {
'getActionForm',
'executeAction',
'listWorkflows',
'triggerWorkflow',
];

const enabled = new Set(options?.enabledTools ?? allToolNames);
Expand Down
Loading
Loading