Fix extension headers (#5000)

This commit is contained in:
Amed Rodriguez
2025-10-03 15:06:54 -07:00
committed by GitHub
parent 1393783f6e
commit 8d7aed9f6b
3 changed files with 128 additions and 10 deletions
@@ -0,0 +1,89 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ExtensionModal from './ExtensionModal';
import { ExtensionFormData } from '../utils';
describe('ExtensionModal', () => {
it('creates a http_streamable extension', async () => {
const user = userEvent.setup();
const mockOnSubmit = vi.fn();
const mockOnClose = vi.fn();
const initialData: ExtensionFormData = {
name: '',
description: '',
type: 'stdio', // Default type
cmd: '',
endpoint: '',
enabled: true,
timeout: 300,
envVars: [],
headers: [],
};
render(
<ExtensionModal
title="Add custom extension"
initialData={initialData}
onClose={mockOnClose}
onSubmit={mockOnSubmit}
submitLabel="Add Extension"
modalType="add"
/>
);
const nameInput = screen.getByPlaceholderText('Enter extension name...');
const submitButton = screen.getByTestId('extension-submit-btn');
await user.type(nameInput, 'Test MCP');
const typeSelect = screen.getByRole('combobox');
await user.click(typeSelect);
const httpOption = screen.getByText('Streamable HTTP');
await user.click(httpOption);
await waitFor(() => {
expect(screen.getByText('Request Headers')).toBeInTheDocument();
});
const endpointInput = screen.getByPlaceholderText('Enter endpoint URL...');
await user.type(endpointInput, 'https://foo.bar.com/mcp/');
const descriptionInput = screen.getByPlaceholderText('Optional description...');
await user.type(descriptionInput, 'Test MCP extension');
const headerNameInput = screen.getByPlaceholderText('Header name');
const headerValueInput = screen
.getAllByPlaceholderText('Value')
.find(
(input) =>
input.closest('div')?.textContent?.includes('Request Headers') ||
input.parentElement?.parentElement?.textContent?.includes('Request Headers')
);
await user.type(headerNameInput, 'Authorization');
if (headerValueInput) {
await user.type(headerValueInput, 'Bearer abc123');
}
await user.click(submitButton);
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalled();
});
const submittedData = mockOnSubmit.mock.calls[0][0];
expect(submittedData.name).toBe('Test MCP');
expect(submittedData.type).toBe('streamable_http');
expect(submittedData.endpoint).toBe('https://foo.bar.com/mcp/');
expect(submittedData.description).toBe('Test MCP extension');
expect(submittedData.timeout).toBe(300);
expect(submittedData.headers).toHaveLength(1);
expect(submittedData.headers).toEqual([
{ key: 'Authorization', value: 'Bearer abc123', isEdited: true },
]);
});
});
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useCallback } from 'react';
import { Button } from '../../../ui/button';
import {
Dialog,
@@ -43,6 +43,7 @@ export default function ExtensionModal({
const [showCloseConfirmation, setShowCloseConfirmation] = useState(false);
const [hasPendingEnvVars, setHasPendingEnvVars] = useState(false);
const [hasPendingHeaders, setHasPendingHeaders] = useState(false);
const [pendingHeader, setPendingHeader] = useState<{ key: string; value: string } | null>(null);
// Function to check if form has been modified
const hasFormChanges = (): boolean => {
@@ -72,7 +73,7 @@ export default function ExtensionModal({
envVar.value !== '••••••••'
);
// Check if there are pending environment variables being typed
// Check if there are pending environment variables or headers being typed
const hasPendingInput = hasPendingEnvVars || hasPendingHeaders;
return (
@@ -168,6 +169,14 @@ export default function ExtensionModal({
});
};
const handlePendingHeaderChange = useCallback(
(hasPending: boolean, header: { key: string; value: string } | null) => {
setHasPendingHeaders(hasPending);
setPendingHeader(header);
},
[]
);
// Function to store a secret value
const storeSecret = async (key: string, value: string) => {
try {
@@ -217,8 +226,16 @@ export default function ExtensionModal({
);
};
const getFinalHeaders = () => {
const finalHeaders = [...formData.headers];
if (pendingHeader && pendingHeader.key.trim() !== '' && pendingHeader.value.trim() !== '') {
finalHeaders.push({ ...pendingHeader, isEdited: true });
}
return finalHeaders;
};
const isHeadersValid = () => {
return formData.headers.every(
return getFinalHeaders().every(
({ key, value }) => (key === '' && value === '') || (key !== '' && value !== '')
);
};
@@ -249,8 +266,13 @@ export default function ExtensionModal({
setSubmitAttempted(true);
if (isFormValid()) {
const finalFormData = {
...formData,
headers: getFinalHeaders(),
};
// Only store env vars that have been edited (which includes new)
const secretPromises = formData.envVars
const secretPromises = finalFormData.envVars
.filter((envVar) => envVar.isEdited)
.map(({ key, value }) => storeSecret(key, value));
@@ -261,9 +283,11 @@ export default function ExtensionModal({
if (results.every((success) => success)) {
// Convert timeout to number if needed
const dataToSubmit = {
...formData,
...finalFormData,
timeout:
typeof formData.timeout === 'string' ? Number(formData.timeout) : formData.timeout,
typeof finalFormData.timeout === 'string'
? Number(finalFormData.timeout)
: finalFormData.timeout,
};
onSubmit(dataToSubmit);
onClose();
@@ -366,7 +390,7 @@ export default function ExtensionModal({
onRemove={handleRemoveHeader}
onChange={handleHeaderChange}
submitAttempted={submitAttempted}
onPendingInputChange={setHasPendingHeaders}
onPendingInputChange={handlePendingHeaderChange}
/>
</div>
</>
@@ -10,7 +10,10 @@ interface HeadersSectionProps {
onRemove: (index: number) => void;
onChange: (index: number, field: 'key' | 'value', value: string) => void;
submitAttempted: boolean;
onPendingInputChange?: (hasPending: boolean) => void;
onPendingInputChange: (
hasPendingInput: boolean,
pendingHeader: { key: string; value: string } | null
) => void;
}
export default function HeadersSection({
@@ -29,10 +32,12 @@ export default function HeadersSection({
value: false,
});
// Track pending input changes
// Notify parent when pending input changes
React.useEffect(() => {
const hasPendingInput = newKey.trim() !== '' || newValue.trim() !== '';
onPendingInputChange?.(hasPendingInput);
const pendingHeader =
newKey.trim() && newValue.trim() ? { key: newKey, value: newValue } : null;
onPendingInputChange(hasPendingInput, pendingHeader);
}, [newKey, newValue, onPendingInputChange]);
const handleAdd = () => {