refactor(ui): type the MCP form store snapshot instead of Record<string, any>

The ported call sites read the form store through `Record<string, any>`,
which reproduced antd's untyped `getFieldsValue(true)` boundary and let a
misspelled key through unchecked. Replaces it with a named snapshot type
covering the keys those call sites actually read.
This commit is contained in:
Yuneng Jiang 2026-08-18 22:43:02 -07:00
parent ad3f324f3d
commit 356f73cc01
No known key found for this signature in database
3 changed files with 44 additions and 9 deletions

View file

@ -57,7 +57,7 @@ import OpenAPIFormSection, { OpenAPIKeyTool } from "./OpenAPIFormSection";
import MCPLogoSelector from "./MCPLogoSelector";
import EnvVarsSection from "./EnvVarsSection";
import { isAdminRole } from "@/utils/roles";
import { antdValidator, validateMCPServerUrl, validateMCPServerName } from "./utils";
import { antdValidator, asFormSnapshot, validateMCPServerUrl, validateMCPServerName } from "./utils";
import { toast } from "@/lib/toast";
import { useMcpOAuthFlow } from "@/hooks/useMcpOAuthFlow";
import { useTestMCPConnection } from "@/hooks/useTestMCPConnection";
@ -170,7 +170,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
const persistCreateUiState = () => {
writeCreateUiSnapshot({
modalVisible: isModalVisible,
formValues: form.getValues() as Record<string, any>,
formValues: form.getValues(),
transportType,
costConfig,
allowedTools,
@ -197,7 +197,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
...(dcrClientRef.current ?? {}),
}),
getTemporaryPayload: () => {
const values: Record<string, any> = form.getValues();
const values = asFormSnapshot(form.getValues());
const transport = values.transport || transportType;
// For OpenAPI transport the form has spec_path instead of url.
// We pass the spec_path as url so the temp-session endpoint has something
@ -518,7 +518,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
if (isHeldOAuthTokenStale(form.getValues(), authorizedIdentity)) {
clearHeldOAuthToken();
}
setFormValues(form.getValues() as Record<string, any>);
setFormValues(form.getValues());
};
// Generate options with existing groups and potential new group
@ -616,7 +616,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
}
if (isHeldOAuthTokenStale(form.getValues(), authorizedIdentity)) {
clearHeldOAuthToken(changedValues);
setFormValues(form.getValues() as Record<string, any>);
setFormValues(form.getValues());
return;
}
setFormValues(allValues);

View file

@ -53,7 +53,13 @@ import IdJagFormFields from "./IdJagFormFields";
import OAuthFormFields from "./OAuthFormFields";
import MCPLogoSelector from "./MCPLogoSelector";
import EnvVarsSection from "./EnvVarsSection";
import { antdValidator, validateMCPServerUrl, validateMCPServerName, normalizeToolOverrideMap } from "./utils";
import {
antdValidator,
asFormSnapshot,
validateMCPServerUrl,
validateMCPServerName,
normalizeToolOverrideMap,
} from "./utils";
import { buildEditServerPayload, editPayloadErrorMessage } from "./editServerPayload";
import { toast } from "@/lib/toast";
import { useMcpOAuthFlow } from "@/hooks/useMcpOAuthFlow";
@ -222,7 +228,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
return;
}
try {
const values: Record<string, any> = form.getValues();
const values = asFormSnapshot(form.getValues());
setSecureItem(
EDIT_OAUTH_UI_STATE_KEY,
JSON.stringify({
@ -261,7 +267,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
accessToken,
getCredentials: () => form.getValues("credentials") as Record<string, unknown> | undefined,
getTemporaryPayload: () => {
const values: Record<string, any> = form.getValues();
const values = asFormSnapshot(form.getValues());
const url = values.url || mcpServer.url;
const transport = values.transport || mcpServer.transport;
if (!url || !transport) {
@ -535,7 +541,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
setIsLoadingTools(true);
setToolsError(null);
try {
const values: Record<string, any> = form.getValues();
const values = asFormSnapshot(form.getValues());
const rawTransport = values.transport || mcpServer.transport;
// oauth2_flow must be explicit: the preview endpoint infers client_credentials from the
// inherited client_id/client_secret/token_url (common once DCR or discovery filled them) and

View file

@ -116,3 +116,32 @@ export const normalizeToolOverrideMap = (
}
return value;
};
export interface StaticHeaderEntry {
readonly header?: string;
readonly value?: string;
}
export interface ServerFormSnapshot {
readonly alias?: string;
readonly args?: string;
readonly auth_type?: string;
readonly authorization_url?: string;
readonly command?: string;
readonly credentials?: Record<string, unknown>;
readonly delegate_auth_to_upstream?: boolean;
readonly description?: string;
readonly env?: Record<string, string>;
readonly issuer?: string;
readonly mcp_access_groups?: readonly string[];
readonly oauth_flow_type?: string;
readonly registration_url?: string;
readonly server_name?: string;
readonly spec_path?: string;
readonly static_headers?: readonly StaticHeaderEntry[];
readonly token_url?: string;
readonly transport?: string;
readonly url?: string;
}
export const asFormSnapshot = (values: Record<string, unknown>): ServerFormSnapshot => values as ServerFormSnapshot;