Skip to content

flowing env vars to the client app for inspector #258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
10 changes: 8 additions & 2 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node

import { resolve, dirname } from "path";
import { dirname, resolve } from "path";
import { spawnPromise } from "spawn-rx";
import { fileURLToPath } from "url";

Expand Down Expand Up @@ -63,6 +63,7 @@ async function main() {

const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274";
const SERVER_PORT = process.env.SERVER_PORT ?? "6277";
const MCP_PROXY_FULL_ADDRESS = process.env.MCP_PROXY_FULL_ADDRESS ?? "";

console.log("Starting MCP inspector...");

Expand Down Expand Up @@ -100,7 +101,12 @@ async function main() {
if (serverOk) {
try {
await spawnPromise("node", [inspectorClientPath], {
env: { ...process.env, PORT: CLIENT_PORT },
env: {
...process.env,
PORT: CLIENT_PORT,
MCP_PROXY_FULL_ADDRESS,
SERVER_PORT,
},
signal: abort.signal,
echoOutput: true,
});
Expand Down
110 changes: 103 additions & 7 deletions client/bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,114 @@
#!/usr/bin/env node

import { join, dirname } from "path";
import { fileURLToPath } from "url";
import handler from "serve-handler";
import fs from "fs";
import http from "http";
import https from "https";
import { dirname, join } from "path";
import handler from "serve-handler";
import { fileURLToPath } from "url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const distPath = join(__dirname, "../dist");

// Function to determine the MCP server URL
const getMcpServerUrl = () => {
if (process.env.MCP_PROXY_FULL_ADDRESS) {
return process.env.MCP_PROXY_FULL_ADDRESS;
}

// Use current host with custom port if specified
const port = process.env.SERVER_PORT || "6277";
// Default to http://localhost:port
return `http://localhost:${port}`;
};

const server = http.createServer((request, response) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a better approach be to expose an endpoint in this server to return the config ? That would simplify this part of the code (and not have to deal with runtime injection).

Something like that is being done here (although it contacts the proxy server):

fetch(`${getMCPProxyAddress(config)}/config`)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runtime config stuff further down in the client code definitely would be simpler.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in, create an API in the client/bin/cli.js that provides an endpoint in which you load the config file? It would but doesn't that add overhead to the startup of the app in that you have to make an API call to get the config, then make an API call to get the config from the server.

Maybe they could be combined, make the /config exist on the client app, which then proxies the SERVER/config.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aaronpowell Could we get a mermaid sequence diagram showing how this is intended to work? That would give us a better starting point for understanding how best to plumb this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sequenceDiagram
    participant SPA
    participant ClientHost
    participant ProxyServer
    
    Note over SPA: Application starts
    SPA->>ClientHost: GET /config
    activate ClientHost
    ClientHost->>ProxyServer: GET /config
    activate ProxyServer
    ProxyServer-->>ClientHost: Return configuration data
    deactivate ProxyServer
    ClientHost-->>SPA: Return configuration data
    deactivate ClientHost
    Note over SPA: Process configuration
Loading

With this we can simplify the "Client Host" (the server run from client/bin/cli.js to not have to worry about much heavy lifting other than "where is the server endpoint" and pass through the request.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the proxy server address part of the config?
Screenshot 2025-04-08 at 1 19 31 PM

return handler(request, response, {
public: distPath,
rewrites: [{ source: "/**", destination: "/index.html" }],
});
// Handle the /config endpoint as a proxy to the MCP server
if (request.url === "/config") {
const mcpServerUrl = getMcpServerUrl();
const configUrl = `${mcpServerUrl}/config`;

try {
const clientModule = mcpServerUrl.startsWith("https:") ? https : http;
const proxyReq = clientModule.request(configUrl, (proxyRes) => {
// Capture the response data to modify it
let data = "";
proxyRes.on("data", (chunk) => {
data += chunk;
});

proxyRes.on("end", () => {
try {
// Parse the JSON response
const jsonResponse = JSON.parse(data);

// Add the MCP_PROXY_FULL_ADDRESS to the response
jsonResponse.config = {
MCP_PROXY_FULL_ADDRESS: { value: mcpServerUrl },
};

// Send the modified response
response.writeHead(proxyRes.statusCode, {
"Content-Type": "application/json",
});
response.end(JSON.stringify(jsonResponse));
} catch (e) {
// If parsing fails, just forward the original response
response.writeHead(proxyRes.statusCode, proxyRes.headers);
response.end(data);
}
});
});

proxyReq.on("error", (err) => {
console.error(`Error proxying request to ${configUrl}:`, err.message);
response.statusCode = 500;
response.end(
JSON.stringify({
error: "Failed to connect to MCP server",
defaultEnvironment: {},
mcpServerUrl: mcpServerUrl,
}),
);
});

request.pipe(proxyReq);
} catch (error) {
console.error(`Error setting up proxy to ${configUrl}:`, error.message);
response.statusCode = 500;
response.end(
JSON.stringify({
error: "Failed to connect to MCP server",
defaultEnvironment: {},
mcpServerUrl: mcpServerUrl,
}),
);
}
}
// Check if this is a request for index.html (either directly or via SPA routing)
else if (
request.url === "/" ||
request.url === "/index.html" ||
!request.url.includes(".")
) {
const indexPath = join(distPath, "index.html");
fs.readFile(indexPath, "utf-8", (err, data) => {
if (err) {
response.statusCode = 500;
response.end(`Error loading index.html: ${err.message}`);
return;
}

response.setHeader("Content-Type", "text/html");
response.end(data);
});
} else {
// For all other assets, use serve-handler as before
return handler(request, response, {
public: distPath,
rewrites: [{ source: "/**", destination: "/index.html" }],
});
}
});

const port = process.env.PORT || 6274;
Expand Down
37 changes: 24 additions & 13 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,7 @@ import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab";
import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants";
import { InspectorConfig } from "./lib/configurationTypes";
import {
getMCPProxyAddress,
getMCPServerRequestTimeout,
} from "./utils/configUtils";
import { getMCPServerRequestTimeout } from "./utils/configUtils";
import { useToast } from "@/hooks/use-toast";

const params = new URLSearchParams(window.location.search);
Expand Down Expand Up @@ -97,13 +94,14 @@ const App = () => {

const [config, setConfig] = useState<InspectorConfig>(() => {
const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY);
if (savedConfig) {
return {
...DEFAULT_INSPECTOR_CONFIG,
...JSON.parse(savedConfig),
} as InspectorConfig;
}
return DEFAULT_INSPECTOR_CONFIG;
let configFromStorage = savedConfig
? ({
...DEFAULT_INSPECTOR_CONFIG,
...JSON.parse(savedConfig),
} as InspectorConfig)
: DEFAULT_INSPECTOR_CONFIG;

return configFromStorage;
});
const [bearerToken, setBearerToken] = useState<string>(() => {
return localStorage.getItem("lastBearerToken") || "";
Expand Down Expand Up @@ -161,7 +159,7 @@ const App = () => {
sseUrl,
env,
bearerToken,
proxyServerUrl: getMCPProxyAddress(config),
proxyServerUrl: config.MCP_PROXY_FULL_ADDRESS.value as string,
requestTimeout: getMCPServerRequestTimeout(config),
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
Expand Down Expand Up @@ -232,16 +230,29 @@ const App = () => {
}, [connectMcpServer, toast]);

useEffect(() => {
fetch(`${getMCPProxyAddress(config)}/config`)
fetch(`/config`)
.then((response) => response.json())
.then((data) => {
setEnv(data.defaultEnvironment);
setConfig((prev) => {
return {
...prev,
...data.config,
};
});
if (data.defaultCommand) {
setCommand(data.defaultCommand);
}
if (data.defaultArgs) {
setArgs(data.defaultArgs);
}

if (data.defaultTransportType) {
setTransportType(data.defaultTransportType);
if (data.defaultTransportType === "sse") {
setSseUrl(data.defaultCommand);
}
}
})
.catch((error) =>
console.error("Error fetching default environment:", error),
Expand Down
4 changes: 2 additions & 2 deletions client/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
},
MCP_PROXY_FULL_ADDRESS: {
description:
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
value: "",
"The endpoint of the MCP Proxy server. This is used to connect to the MCP server.",
value: `http://${window.location.hostname}:${DEFAULT_MCP_PROXY_LISTEN_PORT}`,
},
} as const;
9 changes: 0 additions & 9 deletions client/src/utils/configUtils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
import { InspectorConfig } from "@/lib/configurationTypes";
import { DEFAULT_MCP_PROXY_LISTEN_PORT } from "@/lib/constants";

export const getMCPProxyAddress = (config: InspectorConfig): string => {
const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS.value as string;
if (proxyFullAddress) {
return proxyFullAddress;
}
return `${window.location.protocol}//${window.location.hostname}:${DEFAULT_MCP_PROXY_LISTEN_PORT}`;
};

export const getMCPServerRequestTimeout = (config: InspectorConfig): number => {
return config.MCP_SERVER_REQUEST_TIMEOUT.value as number;
Expand Down
1 change: 0 additions & 1 deletion client/src/vite-env.d.ts

This file was deleted.

Loading