Merge branch 'main' into parse-tool-arg
This commit is contained in:
commit
9da9f01e37
5 changed files with 181 additions and 7 deletions
|
|
@ -877,4 +877,129 @@ describe("useConnection", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("MCP_PROXY_FULL_ADDRESS Configuration", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
// Reset the mock transport objects
|
||||||
|
mockSSETransport.url = undefined;
|
||||||
|
mockSSETransport.options = undefined;
|
||||||
|
mockStreamableHTTPTransport.url = undefined;
|
||||||
|
mockStreamableHTTPTransport.options = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sends proxyFullAddress query parameter for stdio transport when configured", async () => {
|
||||||
|
const propsWithProxyFullAddress = {
|
||||||
|
...defaultProps,
|
||||||
|
transportType: "stdio" as const,
|
||||||
|
command: "test-command",
|
||||||
|
args: "test-args",
|
||||||
|
env: {},
|
||||||
|
config: {
|
||||||
|
...DEFAULT_INSPECTOR_CONFIG,
|
||||||
|
MCP_PROXY_FULL_ADDRESS: {
|
||||||
|
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
|
||||||
|
value: "https://example.com/inspector/mcp_proxy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useConnection(propsWithProxyFullAddress),
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that the URL contains the proxyFullAddress parameter
|
||||||
|
expect(mockSSETransport.url?.searchParams.get("proxyFullAddress")).toBe(
|
||||||
|
"https://example.com/inspector/mcp_proxy",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sends proxyFullAddress query parameter for sse transport when configured", async () => {
|
||||||
|
const propsWithProxyFullAddress = {
|
||||||
|
...defaultProps,
|
||||||
|
transportType: "sse" as const,
|
||||||
|
sseUrl: "http://localhost:8080",
|
||||||
|
config: {
|
||||||
|
...DEFAULT_INSPECTOR_CONFIG,
|
||||||
|
MCP_PROXY_FULL_ADDRESS: {
|
||||||
|
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
|
||||||
|
value: "https://example.com/inspector/mcp_proxy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useConnection(propsWithProxyFullAddress),
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that the URL contains the proxyFullAddress parameter
|
||||||
|
expect(mockSSETransport.url?.searchParams.get("proxyFullAddress")).toBe(
|
||||||
|
"https://example.com/inspector/mcp_proxy",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not send proxyFullAddress parameter when MCP_PROXY_FULL_ADDRESS is empty", async () => {
|
||||||
|
const propsWithEmptyProxy = {
|
||||||
|
...defaultProps,
|
||||||
|
transportType: "stdio" as const,
|
||||||
|
command: "test-command",
|
||||||
|
args: "test-args",
|
||||||
|
env: {},
|
||||||
|
config: {
|
||||||
|
...DEFAULT_INSPECTOR_CONFIG,
|
||||||
|
MCP_PROXY_FULL_ADDRESS: {
|
||||||
|
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConnection(propsWithEmptyProxy));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that the URL does not contain the proxyFullAddress parameter
|
||||||
|
expect(
|
||||||
|
mockSSETransport.url?.searchParams.get("proxyFullAddress"),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not send proxyFullAddress parameter for streamable-http transport", async () => {
|
||||||
|
const propsWithStreamableHttp = {
|
||||||
|
...defaultProps,
|
||||||
|
transportType: "streamable-http" as const,
|
||||||
|
sseUrl: "http://localhost:8080",
|
||||||
|
config: {
|
||||||
|
...DEFAULT_INSPECTOR_CONFIG,
|
||||||
|
MCP_PROXY_FULL_ADDRESS: {
|
||||||
|
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
|
||||||
|
value: "https://example.com/inspector/mcp_proxy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useConnection(propsWithStreamableHttp),
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that streamable-http transport doesn't get proxyFullAddress parameter
|
||||||
|
expect(
|
||||||
|
mockStreamableHTTPTransport.url?.searchParams.get("proxyFullAddress"),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -409,11 +409,20 @@ export function useConnection({
|
||||||
|
|
||||||
let mcpProxyServerUrl;
|
let mcpProxyServerUrl;
|
||||||
switch (transportType) {
|
switch (transportType) {
|
||||||
case "stdio":
|
case "stdio": {
|
||||||
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);
|
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);
|
||||||
mcpProxyServerUrl.searchParams.append("command", command);
|
mcpProxyServerUrl.searchParams.append("command", command);
|
||||||
mcpProxyServerUrl.searchParams.append("args", args);
|
mcpProxyServerUrl.searchParams.append("args", args);
|
||||||
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env));
|
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env));
|
||||||
|
|
||||||
|
const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS
|
||||||
|
.value as string;
|
||||||
|
if (proxyFullAddress) {
|
||||||
|
mcpProxyServerUrl.searchParams.append(
|
||||||
|
"proxyFullAddress",
|
||||||
|
proxyFullAddress,
|
||||||
|
);
|
||||||
|
}
|
||||||
transportOptions = {
|
transportOptions = {
|
||||||
authProvider: serverAuthProvider,
|
authProvider: serverAuthProvider,
|
||||||
eventSourceInit: {
|
eventSourceInit: {
|
||||||
|
|
@ -431,10 +440,20 @@ export function useConnection({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "sse":
|
case "sse": {
|
||||||
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
|
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
|
||||||
mcpProxyServerUrl.searchParams.append("url", sseUrl);
|
mcpProxyServerUrl.searchParams.append("url", sseUrl);
|
||||||
|
|
||||||
|
const proxyFullAddressSSE = config.MCP_PROXY_FULL_ADDRESS
|
||||||
|
.value as string;
|
||||||
|
if (proxyFullAddressSSE) {
|
||||||
|
mcpProxyServerUrl.searchParams.append(
|
||||||
|
"proxyFullAddress",
|
||||||
|
proxyFullAddressSSE,
|
||||||
|
);
|
||||||
|
}
|
||||||
transportOptions = {
|
transportOptions = {
|
||||||
eventSourceInit: {
|
eventSourceInit: {
|
||||||
fetch: (
|
fetch: (
|
||||||
|
|
@ -451,6 +470,7 @@ export function useConnection({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "streamable-http":
|
case "streamable-http":
|
||||||
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);
|
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,22 @@ export type DataType =
|
||||||
| "array"
|
| "array"
|
||||||
| "null";
|
| "null";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the specific data type of a JSON value
|
||||||
|
* @param value The JSON value to analyze
|
||||||
|
* @returns The specific data type including "array" and "null" as distinct types
|
||||||
|
*/
|
||||||
export function getDataType(value: JsonValue): DataType {
|
export function getDataType(value: JsonValue): DataType {
|
||||||
if (Array.isArray(value)) return "array";
|
if (Array.isArray(value)) return "array";
|
||||||
if (value === null) return "null";
|
if (value === null) return "null";
|
||||||
return typeof value;
|
return typeof value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to parse a string as JSON, only for objects and arrays
|
||||||
|
* @param str The string to parse
|
||||||
|
* @returns Object with success boolean and either parsed data or original string
|
||||||
|
*/
|
||||||
export function tryParseJson(str: string): {
|
export function tryParseJson(str: string): {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data: JsonValue;
|
data: JsonValue;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,11 @@ type CallbackParams =
|
||||||
error_uri: string | null;
|
error_uri: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses OAuth 2.1 callback parameters from a URL search string
|
||||||
|
* @param location The URL search string (e.g., "?code=abc123" or "?error=access_denied")
|
||||||
|
* @returns Parsed callback parameters with success/error information
|
||||||
|
*/
|
||||||
export const parseOAuthCallbackParams = (location: string): CallbackParams => {
|
export const parseOAuthCallbackParams = (location: string): CallbackParams => {
|
||||||
const params = new URLSearchParams(location);
|
const params = new URLSearchParams(location);
|
||||||
|
|
||||||
|
|
@ -62,6 +67,11 @@ export const generateOAuthState = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a human-readable error description from OAuth callback error parameters
|
||||||
|
* @param params OAuth error callback parameters containing error details
|
||||||
|
* @returns Formatted multiline error message with error code, description, and optional URI
|
||||||
|
*/
|
||||||
export const generateOAuthErrorDescription = (
|
export const generateOAuthErrorDescription = (
|
||||||
params: Extract<CallbackParams, { successful: false }>,
|
params: Extract<CallbackParams, { successful: false }>,
|
||||||
): string => {
|
): string => {
|
||||||
|
|
|
||||||
|
|
@ -378,7 +378,6 @@ app.get(
|
||||||
let serverTransport: Transport | undefined;
|
let serverTransport: Transport | undefined;
|
||||||
try {
|
try {
|
||||||
serverTransport = await createTransport(req);
|
serverTransport = await createTransport(req);
|
||||||
console.log("Created server transport");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof SseError && error.code === 401) {
|
if (error instanceof SseError && error.code === 401) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|
@ -391,11 +390,16 @@ app.get(
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const webAppTransport = new SSEServerTransport("/message", res);
|
const proxyFullAddress = (req.query.proxyFullAddress as string) || "";
|
||||||
|
const prefix = proxyFullAddress || "";
|
||||||
|
const endpoint = `${prefix}/message`;
|
||||||
|
|
||||||
|
const webAppTransport = new SSEServerTransport(endpoint, res);
|
||||||
|
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
|
||||||
console.log("Created client transport");
|
console.log("Created client transport");
|
||||||
|
|
||||||
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
|
|
||||||
serverTransports.set(webAppTransport.sessionId, serverTransport);
|
serverTransports.set(webAppTransport.sessionId, serverTransport);
|
||||||
|
console.log("Created server transport");
|
||||||
|
|
||||||
await webAppTransport.start();
|
await webAppTransport.start();
|
||||||
|
|
||||||
|
|
@ -484,7 +488,7 @@ app.get(
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
console.log(
|
console.log(
|
||||||
"New SSE connection request. NOTE: The sse transport is deprecated and has been replaced by StreamableHttp",
|
"New SSE connection request. NOTE: The SSE transport is deprecated and has been replaced by StreamableHttp",
|
||||||
);
|
);
|
||||||
let serverTransport: Transport | undefined;
|
let serverTransport: Transport | undefined;
|
||||||
try {
|
try {
|
||||||
|
|
@ -511,9 +515,14 @@ app.get(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverTransport) {
|
if (serverTransport) {
|
||||||
const webAppTransport = new SSEServerTransport("/message", res);
|
const proxyFullAddress = (req.query.proxyFullAddress as string) || "";
|
||||||
|
const prefix = proxyFullAddress || "";
|
||||||
|
const endpoint = `${prefix}/message`;
|
||||||
|
|
||||||
|
const webAppTransport = new SSEServerTransport(endpoint, res);
|
||||||
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
|
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
|
||||||
console.log("Created client transport");
|
console.log("Created client transport");
|
||||||
|
|
||||||
serverTransports.set(webAppTransport.sessionId, serverTransport!);
|
serverTransports.set(webAppTransport.sessionId, serverTransport!);
|
||||||
console.log("Created server transport");
|
console.log("Created server transport");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue