Compare commits
3 Commits
create_doc
...
sse_custom
| Author | SHA1 | Date | |
|---|---|---|---|
| d3c0217681 | |||
| c843005928 | |||
| b2258a1e83 |
64
.github/workflows/main.yml
vendored
64
.github/workflows/main.yml
vendored
@@ -1,64 +0,0 @@
|
|||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Check formatting
|
|
||||||
run: npx prettier --check .
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
cache: npm
|
|
||||||
|
|
||||||
# Working around https://github.com/npm/cli/issues/4828
|
|
||||||
# - run: npm ci
|
|
||||||
- run: npm install --no-package-lock
|
|
||||||
|
|
||||||
- name: Check linting
|
|
||||||
working-directory: ./client
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
- name: Run client tests
|
|
||||||
working-directory: ./client
|
|
||||||
run: npm test
|
|
||||||
|
|
||||||
- run: npm run build
|
|
||||||
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
environment: release
|
|
||||||
needs: build
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
cache: npm
|
|
||||||
registry-url: "https://registry.npmjs.org"
|
|
||||||
|
|
||||||
# Working around https://github.com/npm/cli/issues/4828
|
|
||||||
# - run: npm ci
|
|
||||||
- run: npm install --no-package-lock
|
|
||||||
|
|
||||||
# TODO: Add --provenance once the repo is public
|
|
||||||
- run: npm run publish-all
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
17
Dockerfile
17
Dockerfile
@@ -4,7 +4,21 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN npm ci --ignore-scripts && \
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
|
||||||
|
RUN cd client && \
|
||||||
|
npm install && \
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
|
||||||
|
RUN cd server && \
|
||||||
|
npm install && \
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
RUN cd cli && \
|
||||||
|
npm install && \
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
@@ -22,5 +36,4 @@ RUN npm ci --omit=dev --ignore-scripts
|
|||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
|
||||||
CMD ["node", "client/bin/start.js"]
|
CMD ["node", "client/bin/start.js"]
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^14.2.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.7.5",
|
"@types/node": "^22.7.5",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
|
|||||||
@@ -109,6 +109,11 @@ const App = () => {
|
|||||||
return localStorage.getItem("lastHeaderName") || "";
|
return localStorage.getItem("lastHeaderName") || "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
|
||||||
|
const saved = localStorage.getItem("lastCustomHeaders");
|
||||||
|
return saved ? JSON.parse(saved) : [];
|
||||||
|
});
|
||||||
|
|
||||||
const [pendingSampleRequests, setPendingSampleRequests] = useState<
|
const [pendingSampleRequests, setPendingSampleRequests] = useState<
|
||||||
Array<
|
Array<
|
||||||
PendingRequest & {
|
PendingRequest & {
|
||||||
@@ -183,6 +188,7 @@ const App = () => {
|
|||||||
env,
|
env,
|
||||||
bearerToken,
|
bearerToken,
|
||||||
headerName,
|
headerName,
|
||||||
|
customHeaders,
|
||||||
config,
|
config,
|
||||||
onNotification: (notification) => {
|
onNotification: (notification) => {
|
||||||
setNotifications((prev) => [...prev, notification as ServerNotification]);
|
setNotifications((prev) => [...prev, notification as ServerNotification]);
|
||||||
@@ -226,6 +232,10 @@ const App = () => {
|
|||||||
localStorage.setItem("lastHeaderName", headerName);
|
localStorage.setItem("lastHeaderName", headerName);
|
||||||
}, [headerName]);
|
}, [headerName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("lastCustomHeaders", JSON.stringify(customHeaders));
|
||||||
|
}, [customHeaders]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
|
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
|
||||||
}, [config]);
|
}, [config]);
|
||||||
@@ -581,6 +591,8 @@ const App = () => {
|
|||||||
setBearerToken={setBearerToken}
|
setBearerToken={setBearerToken}
|
||||||
headerName={headerName}
|
headerName={headerName}
|
||||||
setHeaderName={setHeaderName}
|
setHeaderName={setHeaderName}
|
||||||
|
customHeaders={customHeaders}
|
||||||
|
setCustomHeaders={setCustomHeaders}
|
||||||
onConnect={connectMcpServer}
|
onConnect={connectMcpServer}
|
||||||
onDisconnect={disconnectMcpServer}
|
onDisconnect={disconnectMcpServer}
|
||||||
stdErrNotifications={stdErrNotifications}
|
stdErrNotifications={stdErrNotifications}
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useToast } from "../lib/hooks/useToast";
|
import { useToast } from "../lib/hooks/useToast";
|
||||||
|
|
||||||
interface SidebarProps {
|
export interface SidebarProps {
|
||||||
|
|
||||||
connectionStatus: ConnectionStatus;
|
connectionStatus: ConnectionStatus;
|
||||||
transportType: "stdio" | "sse" | "streamable-http";
|
transportType: "stdio" | "sse" | "streamable-http";
|
||||||
setTransportType: (type: "stdio" | "sse" | "streamable-http") => void;
|
setTransportType: (type: "stdio" | "sse" | "streamable-http") => void;
|
||||||
@@ -56,6 +57,8 @@ interface SidebarProps {
|
|||||||
setBearerToken: (token: string) => void;
|
setBearerToken: (token: string) => void;
|
||||||
headerName?: string;
|
headerName?: string;
|
||||||
setHeaderName?: (name: string) => void;
|
setHeaderName?: (name: string) => void;
|
||||||
|
customHeaders: [string, string][];
|
||||||
|
setCustomHeaders: (headers: [string, string][]) => void;
|
||||||
onConnect: () => void;
|
onConnect: () => void;
|
||||||
onDisconnect: () => void;
|
onDisconnect: () => void;
|
||||||
stdErrNotifications: StdErrNotification[];
|
stdErrNotifications: StdErrNotification[];
|
||||||
@@ -83,6 +86,8 @@ const Sidebar = ({
|
|||||||
setBearerToken,
|
setBearerToken,
|
||||||
headerName,
|
headerName,
|
||||||
setHeaderName,
|
setHeaderName,
|
||||||
|
customHeaders,
|
||||||
|
setCustomHeaders,
|
||||||
onConnect,
|
onConnect,
|
||||||
onDisconnect,
|
onDisconnect,
|
||||||
stdErrNotifications,
|
stdErrNotifications,
|
||||||
@@ -101,6 +106,7 @@ const Sidebar = ({
|
|||||||
const [copiedServerEntry, setCopiedServerEntry] = useState(false);
|
const [copiedServerEntry, setCopiedServerEntry] = useState(false);
|
||||||
const [copiedServerFile, setCopiedServerFile] = useState(false);
|
const [copiedServerFile, setCopiedServerFile] = useState(false);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [showCustomHeaders, setShowCustomHeaders] = useState(false);
|
||||||
|
|
||||||
// Reusable error reporter for copy actions
|
// Reusable error reporter for copy actions
|
||||||
const reportError = useCallback(
|
const reportError = useCallback(
|
||||||
@@ -213,6 +219,22 @@ const Sidebar = ({
|
|||||||
}
|
}
|
||||||
}, [generateMCPServerFile, toast, reportError]);
|
}, [generateMCPServerFile, toast, reportError]);
|
||||||
|
|
||||||
|
const removeCustomHeader = (index: number) => {
|
||||||
|
const newHeaders = [...customHeaders];
|
||||||
|
newHeaders.splice(index, 1);
|
||||||
|
setCustomHeaders(newHeaders);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCustomHeader = (index: number, field: 'key' | 'value', value: string) => {
|
||||||
|
const newArr = [...customHeaders];
|
||||||
|
const [oldKey, oldValue] = newArr[index];
|
||||||
|
const newTuple: [string, string] = field === 'key'
|
||||||
|
? [value, oldValue]
|
||||||
|
: [oldKey, value];
|
||||||
|
newArr[index] = newTuple;
|
||||||
|
setCustomHeaders(newArr);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
|
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800">
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800">
|
||||||
@@ -720,6 +742,62 @@ const Sidebar = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowCustomHeaders(!showCustomHeaders)}
|
||||||
|
className="flex items-center w-full"
|
||||||
|
data-testid="custom-headers-button"
|
||||||
|
aria-expanded={showCustomHeaders}
|
||||||
|
>
|
||||||
|
{showCustomHeaders ? (
|
||||||
|
<ChevronDown className="w-4 h-4 mr-2" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Custom Headers
|
||||||
|
</Button>
|
||||||
|
{showCustomHeaders && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{customHeaders.map((header, index) => (
|
||||||
|
<div key={index} className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Header Name</label>
|
||||||
|
<Input
|
||||||
|
placeholder="Header Name"
|
||||||
|
value={header[0]}
|
||||||
|
onChange={(e) => updateCustomHeader(index, 'key', e.target.value)}
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<label className="text-sm font-medium">Header Value</label>
|
||||||
|
<Input
|
||||||
|
placeholder="Header Value"
|
||||||
|
value={header[1]}
|
||||||
|
onChange={(e) => updateCustomHeader(index, 'value', e.target.value)}
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeCustomHeader(index)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Remove Header
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full mt-2"
|
||||||
|
onClick={() => {
|
||||||
|
setCustomHeaders([ ...customHeaders, ["", ""]]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Custom Header
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 border-t">
|
<div className="p-4 border-t">
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ Object.defineProperty(navigator, "clipboard", {
|
|||||||
// Setup fake timers
|
// Setup fake timers
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
describe("Sidebar Environment Variables", () => {
|
const defaultProps = {
|
||||||
const defaultProps = {
|
|
||||||
connectionStatus: "disconnected" as const,
|
connectionStatus: "disconnected" as const,
|
||||||
transportType: "stdio" as const,
|
transportType: "stdio" as const,
|
||||||
setTransportType: jest.fn(),
|
setTransportType: jest.fn(),
|
||||||
@@ -46,17 +45,22 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
setEnv: jest.fn(),
|
setEnv: jest.fn(),
|
||||||
bearerToken: "",
|
bearerToken: "",
|
||||||
setBearerToken: jest.fn(),
|
setBearerToken: jest.fn(),
|
||||||
|
headerName: "",
|
||||||
|
setHeaderName: jest.fn(),
|
||||||
|
customHeaders: [],
|
||||||
|
setCustomHeaders: jest.fn(),
|
||||||
onConnect: jest.fn(),
|
onConnect: jest.fn(),
|
||||||
onDisconnect: jest.fn(),
|
onDisconnect: jest.fn(),
|
||||||
stdErrNotifications: [],
|
stdErrNotifications: [],
|
||||||
clearStdErrNotifications: jest.fn(),
|
clearStdErrNotifications: jest.fn(),
|
||||||
logLevel: "info" as const,
|
logLevel: "debug" as const,
|
||||||
sendLogLevelRequest: jest.fn(),
|
sendLogLevelRequest: jest.fn(),
|
||||||
loggingSupported: true,
|
loggingSupported: false,
|
||||||
config: DEFAULT_INSPECTOR_CONFIG,
|
config: DEFAULT_INSPECTOR_CONFIG,
|
||||||
setConfig: jest.fn(),
|
setConfig: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe("Sidebar Environment Variables", () => {
|
||||||
const renderSidebar = (props = {}) => {
|
const renderSidebar = (props = {}) => {
|
||||||
return render(
|
return render(
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -870,3 +874,79 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Sidebar", () => {
|
||||||
|
it("renders", () => {
|
||||||
|
render(<Sidebar {...defaultProps} />);
|
||||||
|
expect(screen.getByText("MCP Inspector")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows connect button when disconnected", () => {
|
||||||
|
render(<Sidebar {...defaultProps} />);
|
||||||
|
expect(screen.getByText("Connect")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows disconnect button when connected", () => {
|
||||||
|
render(
|
||||||
|
<Sidebar
|
||||||
|
{...defaultProps}
|
||||||
|
connectionStatus="connected"
|
||||||
|
customHeaders={[]}
|
||||||
|
setCustomHeaders={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Disconnect")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows reconnect button when connected", () => {
|
||||||
|
render(
|
||||||
|
<Sidebar
|
||||||
|
{...defaultProps}
|
||||||
|
connectionStatus="connected"
|
||||||
|
transportType="sse"
|
||||||
|
customHeaders={[]}
|
||||||
|
setCustomHeaders={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Reconnect")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows restart button when connected with stdio transport", () => {
|
||||||
|
render(
|
||||||
|
<Sidebar
|
||||||
|
{...defaultProps}
|
||||||
|
connectionStatus="connected"
|
||||||
|
transportType="stdio"
|
||||||
|
customHeaders={[]}
|
||||||
|
setCustomHeaders={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Restart")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows environment variables section when stdio transport is selected", () => {
|
||||||
|
render(
|
||||||
|
<Sidebar
|
||||||
|
{...defaultProps}
|
||||||
|
env={{ NEW_KEY: "new_value" }}
|
||||||
|
customHeaders={[]}
|
||||||
|
setCustomHeaders={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const envButton = screen.getByText("Environment Variables");
|
||||||
|
expect(envButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows configuration section", () => {
|
||||||
|
render(
|
||||||
|
<Sidebar
|
||||||
|
{...defaultProps}
|
||||||
|
config={DEFAULT_INSPECTOR_CONFIG}
|
||||||
|
customHeaders={[]}
|
||||||
|
setCustomHeaders={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const configButton = screen.getByText("Configuration");
|
||||||
|
expect(configButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ interface UseConnectionOptions {
|
|||||||
env: Record<string, string>;
|
env: Record<string, string>;
|
||||||
bearerToken?: string;
|
bearerToken?: string;
|
||||||
headerName?: string;
|
headerName?: string;
|
||||||
|
customHeaders?: [string, string][];
|
||||||
config: InspectorConfig;
|
config: InspectorConfig;
|
||||||
onNotification?: (notification: Notification) => void;
|
onNotification?: (notification: Notification) => void;
|
||||||
onStdErrNotification?: (notification: Notification) => void;
|
onStdErrNotification?: (notification: Notification) => void;
|
||||||
@@ -71,6 +72,7 @@ export function useConnection({
|
|||||||
env,
|
env,
|
||||||
bearerToken,
|
bearerToken,
|
||||||
headerName,
|
headerName,
|
||||||
|
customHeaders,
|
||||||
config,
|
config,
|
||||||
onNotification,
|
onNotification,
|
||||||
onStdErrNotification,
|
onStdErrNotification,
|
||||||
@@ -287,7 +289,9 @@ export function useConnection({
|
|||||||
try {
|
try {
|
||||||
// Inject auth manually instead of using SSEClientTransport, because we're
|
// Inject auth manually instead of using SSEClientTransport, because we're
|
||||||
// proxying through the inspector server first.
|
// proxying through the inspector server first.
|
||||||
const headers: HeadersInit = {};
|
const headers = new Headers(customHeaders||[]);
|
||||||
|
|
||||||
|
//const headers: HeadersInit = [ ...customHeaders||[] ];
|
||||||
|
|
||||||
// Create an auth provider with the current server URL
|
// Create an auth provider with the current server URL
|
||||||
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
|
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
|
||||||
@@ -297,7 +301,7 @@ export function useConnection({
|
|||||||
bearerToken || (await serverAuthProvider.tokens())?.access_token;
|
bearerToken || (await serverAuthProvider.tokens())?.access_token;
|
||||||
if (token) {
|
if (token) {
|
||||||
const authHeaderName = headerName || "Authorization";
|
const authHeaderName = headerName || "Authorization";
|
||||||
headers[authHeaderName] = `Bearer ${token}`;
|
headers.set(authHeaderName, `Bearer ${token}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create appropriate transport
|
// Create appropriate transport
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"types": ["jest", "@testing-library/jest-dom", "node"]
|
"types": ["jest", "@testing-library/jest-dom", "node", "react", "react-dom"]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
784
package-lock.json
generated
784
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,14 @@ import { findActualExecutable } from "spawn-rx";
|
|||||||
import mcpProxy from "./mcpProxy.js";
|
import mcpProxy from "./mcpProxy.js";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
const SSE_HEADERS_PASSTHROUGH = ["authorization"];
|
const SSE_HEADERS_PASSTHROUGH = [
|
||||||
|
"authorization",
|
||||||
|
"x-api-key",
|
||||||
|
"x-custom-header",
|
||||||
|
"x-auth-token",
|
||||||
|
"x-request-id",
|
||||||
|
"x-correlation-id"
|
||||||
|
];
|
||||||
const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
|
const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
|
||||||
"authorization",
|
"authorization",
|
||||||
"mcp-session-id",
|
"mcp-session-id",
|
||||||
|
|||||||
Reference in New Issue
Block a user