Let agents drive the flow API over MCP
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s

The engine now speaks MCP at /mcp, with a built-in OAuth 2.1 authorization
server in front of it: an agent registers itself, sends a human to the browser
to approve it, and exchanges the resulting code for a token. PKCE is required,
codes are single-use and stored only as hashes, the browser is redirected to
the URI that was registered rather than the one asked for, and refresh tokens
rotate so that replaying a spent one revokes the whole line.

Twenty tools cover reading, building, publishing and running flows, and each
one calls the same REST endpoint the dashboard calls, in-process, carrying the
caller's own token. That keeps one description of what a flow is and what may
be done to it — validation, the draft/publish split, the version check — and
means an agent can do nothing a person could not do in the browser.

Agent tokens are RS256 with a keypair of their own rather than the secret that
signs browser sessions, so deleting the key withdraws every agent without
logging anyone out, and deps.decode_token grew the branch that trusting a
second issuer will need when the hosted login arrives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Melvin Strobl
2026-08-16 00:22:41 +02:00
co-authored by Claude Fable 5
parent 3724b68f23
commit 8d82d6c4ec
28 changed files with 2459 additions and 555 deletions
+230
View File
@@ -57,6 +57,84 @@ export const Body_login_login_access_tokenSchema = {
title: 'Body_login-login_access_token'
} as const;
export const Body_oauth_tokenSchema = {
properties: {
grant_type: {
type: 'string',
title: 'Grant Type'
},
code: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Code'
},
redirect_uri: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Redirect Uri'
},
client_id: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Client Id'
},
code_verifier: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Code Verifier'
},
refresh_token: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Refresh Token'
},
resource: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Resource'
}
},
type: 'object',
required: ['grant_type'],
title: 'Body_oauth-token'
} as const;
export const DTypeSchema = {
type: 'string',
enum: ['float', 'int', 'str', 'bool', 'json'],
@@ -702,6 +780,158 @@ export const NodeTypeInfoSchema = {
description: 'A node type the editor can offer, with its parameter schema.'
} as const;
export const OAuthAuthorizeInfoSchema = {
properties: {
client_name: {
type: 'string',
title: 'Client Name'
},
redirect_uri: {
type: 'string',
title: 'Redirect Uri'
},
scope: {
type: 'string',
title: 'Scope'
}
},
type: 'object',
required: ['client_name', 'redirect_uri', 'scope'],
title: 'OAuthAuthorizeInfo',
description: 'What the consent page shows, all of it validated server-side.'
} as const;
export const OAuthAuthorizeRequestSchema = {
properties: {
client_id: {
type: 'string',
title: 'Client Id'
},
redirect_uri: {
type: 'string',
title: 'Redirect Uri'
},
code_challenge: {
type: 'string',
title: 'Code Challenge'
},
code_challenge_method: {
type: 'string',
title: 'Code Challenge Method',
default: 'S256'
},
state: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'State'
},
resource: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Resource'
},
scope: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Scope'
}
},
type: 'object',
required: ['client_id', 'redirect_uri', 'code_challenge'],
title: 'OAuthAuthorizeRequest'
} as const;
export const OAuthAuthorizeResponseSchema = {
properties: {
redirect_url: {
type: 'string',
title: 'Redirect Url'
}
},
type: 'object',
required: ['redirect_url'],
title: 'OAuthAuthorizeResponse'
} as const;
export const OAuthClientRegisterSchema = {
properties: {
client_name: {
type: 'string',
maxLength: 128,
title: 'Client Name',
default: 'MCP client'
},
redirect_uris: {
items: {
type: 'string'
},
type: 'array',
title: 'Redirect Uris'
},
grant_types: {
anyOf: [
{
items: {
type: 'string'
},
type: 'array'
},
{
type: 'null'
}
],
title: 'Grant Types'
},
response_types: {
anyOf: [
{
items: {
type: 'string'
},
type: 'array'
},
{
type: 'null'
}
],
title: 'Response Types'
},
token_endpoint_auth_method: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Token Endpoint Auth Method'
}
},
type: 'object',
required: ['redirect_uris'],
title: 'OAuthClientRegister',
description: 'RFC 7591 dynamic client registration request.'
} as const;
export const PositionSchema = {
properties: {
x: {
+89 -1
View File
@@ -3,7 +3,7 @@
import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
import type { FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, PrivateCreateUserData, PrivateCreateUserResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse } from './types.gen';
import type { FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, PrivateCreateUserData, PrivateCreateUserResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse } from './types.gen';
export class FlowsService {
/**
@@ -615,6 +615,94 @@ export class LoginService {
}
}
export class OauthService {
/**
* Register Client
* RFC 7591 dynamic client registration.
*
* Open on purpose, and harmless on its own: a registered client can do
* nothing until a signed-in human approves it on the consent page.
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static registerClient(data: OauthRegisterClientData): CancelablePromise<OauthRegisterClientResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/oauth/register',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Authorize Validate
* What the consent page should say, checked before it says it.
* @param data The data for the request.
* @param data.clientId
* @param data.redirectUri
* @returns OAuthAuthorizeInfo Successful Response
* @throws ApiError
*/
public static authorizeValidate(data: OauthAuthorizeValidateData): CancelablePromise<OauthAuthorizeValidateResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/oauth/authorize/validate',
query: {
client_id: data.clientId,
redirect_uri: data.redirectUri
},
errors: {
422: 'Validation Error'
}
});
}
/**
* Authorize
* Approve a client, on behalf of the signed-in user.
* @param data The data for the request.
* @param data.requestBody
* @returns OAuthAuthorizeResponse Successful Response
* @throws ApiError
*/
public static authorize(data: OauthAuthorizeData): CancelablePromise<OauthAuthorizeResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/oauth/authorize',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Token
* Exchange a code, or a refresh token, for an access token.
* @param data The data for the request.
* @param data.formData
* @returns unknown Successful Response
* @throws ApiError
*/
public static token(data: OauthTokenData): CancelablePromise<OauthTokenResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/oauth/token',
formData: data.formData,
mediaType: 'application/x-www-form-urlencoded',
errors: {
422: 'Validation Error'
}
});
}
}
export class PrivateService {
/**
* Create User
+69
View File
@@ -9,6 +9,16 @@ export type Body_login_login_access_token = {
client_secret?: (string | null);
};
export type Body_oauth_token = {
grant_type: string;
code?: (string | null);
redirect_uri?: (string | null);
client_id?: (string | null);
code_verifier?: (string | null);
refresh_token?: (string | null);
resource?: (string | null);
};
/**
* Serializable payload types.
*
@@ -228,6 +238,40 @@ export type NodeTypeInfo = {
plugin?: (string | null);
};
/**
* What the consent page shows, all of it validated server-side.
*/
export type OAuthAuthorizeInfo = {
client_name: string;
redirect_uri: string;
scope: string;
};
export type OAuthAuthorizeRequest = {
client_id: string;
redirect_uri: string;
code_challenge: string;
code_challenge_method?: string;
state?: (string | null);
resource?: (string | null);
scope?: (string | null);
};
export type OAuthAuthorizeResponse = {
redirect_url: string;
};
/**
* RFC 7591 dynamic client registration request.
*/
export type OAuthClientRegister = {
client_name?: string;
redirect_uris: Array<(string)>;
grant_types?: (Array<(string)> | null);
response_types?: (Array<(string)> | null);
token_endpoint_auth_method?: (string | null);
};
/**
* Where a node sits on the canvas.
*/
@@ -517,6 +561,31 @@ export type LoginRecoverPasswordHtmlContentData = {
export type LoginRecoverPasswordHtmlContentResponse = (string);
export type OauthRegisterClientData = {
requestBody: OAuthClientRegister;
};
export type OauthRegisterClientResponse = (unknown);
export type OauthAuthorizeValidateData = {
clientId: string;
redirectUri: string;
};
export type OauthAuthorizeValidateResponse = (OAuthAuthorizeInfo);
export type OauthAuthorizeData = {
requestBody: OAuthAuthorizeRequest;
};
export type OauthAuthorizeResponse = (OAuthAuthorizeResponse);
export type OauthTokenData = {
formData: Body_oauth_token;
};
export type OauthTokenResponse = (unknown);
export type PrivateCreateUserData = {
requestBody: PrivateUserCreate;
};