Dify Server
by YanxingLiu
- src
#!/usr/bin/env node
import {Server} from "@modelcontextprotocol/sdk/server/index.js";
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
import {CallToolRequestSchema, CallToolResult, ListToolsRequestSchema,} from "@modelcontextprotocol/sdk/types.js";
// Logging is enabled only if LOG_ENABLED environment variable is set to 'true'
const LOG_ENABLED = process.env.LOG_ENABLED === 'true';
const HOST = process.env.HOST ?? "127.0.0.1"
export function log(...args: any[]) {
if (LOG_ENABLED) {
console.error(...args);
}
}
interface IDEResponseOk {
status: string;
error: null;
}
interface IDEResponseErr {
status: null;
error: string;
}
type IDEResponse = IDEResponseOk | IDEResponseErr;
/**
* Globally store the cached IDE endpoint.
* We'll update this once at the beginning and every 10 seconds.
*/
let cachedEndpoint: string | null = null;
/**
* If you need to remember the last known response from /mcp/list_tools, store it here.
* That way, you won't re-check it every single time a new request comes in.
*/
let previousResponse: string | null = null;
/**
* Helper to send the "tools changed" notification.
*/
function sendToolsChanged() {
try {
log("Sending tools changed notification.");
server.notification({method: "notifications/tools/list_changed"});
} catch (error) {
log("Error sending tools changed notification:", error);
}
}
/**
* Test if /mcp/list_tools is responding on a given endpoint
*
* @returns true if working, false otherwise
*/
async function testListTools(endpoint: string): Promise<boolean> {
log(`Sending test request to ${endpoint}/mcp/list_tools`);
try {
const res = await fetch(`${endpoint}/mcp/list_tools`);
if (!res.ok) {
log(`Test request to ${endpoint}/mcp/list_tools failed with status ${res.status}`);
return false;
}
const currentResponse = await res.text();
log(`Received response from ${endpoint}/mcp/list_tools: ${currentResponse.substring(0, 100)}...`);
// If the response changed from last time, notify
if (previousResponse !== null && previousResponse !== currentResponse) {
log("Response has changed since the last check.");
sendToolsChanged();
}
previousResponse = currentResponse;
return true;
} catch (error) {
log(`Error during testListTools for endpoint ${endpoint}:`, error);
return false;
}
}
/**
* Finds and returns a working IDE endpoint using IPv4 by:
* 1. Checking process.env.IDE_PORT, or
* 2. Scanning ports 63342-63352
*
* Throws if none found.
*/
async function findWorkingIDEEndpoint(): Promise<string> {
log("Attempting to find a working IDE endpoint...");
// 1. If user specified a port, just use that
if (process.env.IDE_PORT) {
log(`IDE_PORT is set to ${process.env.IDE_PORT}. Testing this port.`);
const testEndpoint = `http://${HOST}:${process.env.IDE_PORT}/api`;
if (await testListTools(testEndpoint)) {
log(`IDE_PORT ${process.env.IDE_PORT} is working.`);
return testEndpoint;
} else {
log(`Specified IDE_PORT=${process.env.IDE_PORT} but it is not responding correctly.`);
throw new Error(`Specified IDE_PORT=${process.env.IDE_PORT} but it is not responding correctly.`);
}
}
// 2. Reuse existing endpoint if it's still working
if (cachedEndpoint != null && await testListTools(cachedEndpoint)) {
log('Using cached endpoint, it\'s still working')
return cachedEndpoint
}
// 3. Otherwise, scan a range of ports
for (let port = 63342; port <= 63352; port++) {
const candidateEndpoint = `http://${HOST}:${port}/api`;
log(`Testing port ${port}...`);
const isWorking = await testListTools(candidateEndpoint);
if (isWorking) {
log(`Found working IDE endpoint at ${candidateEndpoint}`);
return candidateEndpoint;
} else {
log(`Port ${port} is not responding correctly.`);
}
}
// If we reach here, no port was found
previousResponse = "";
log("No working IDE endpoint found in range 63342-63352");
throw new Error("No working IDE endpoint found in range 63342-63352");
}
/**
* Updates the cached endpoint by finding a working IDE endpoint.
* This runs once at startup and then once every 10 seconds in runServer().
*/
async function updateIDEEndpoint() {
try {
cachedEndpoint = await findWorkingIDEEndpoint();
log(`Updated cachedEndpoint to: ${cachedEndpoint}`);
} catch (error) {
// If we fail to find a working endpoint, keep the old one if it existed.
// It's up to you how to handle this scenario (e.g., set cachedEndpoint = null).
log("Failed to update IDE endpoint:", error);
}
}
/**
* Main MCP server
*/
const server = new Server(
{
name: "jetbrains/proxy",
version: "0.1.0",
},
{
capabilities: {
tools: {
listChanged: true,
},
resources: {},
},
},
);
/**
* Handles listing tools by using the *cached* endpoint (no new search each time).
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
log("Handling ListToolsRequestSchema request.");
if (!cachedEndpoint) {
// If no cached endpoint, we can't proceed
throw new Error("No working IDE endpoint available.");
}
try {
log(`Using cached endpoint ${cachedEndpoint} to list tools.`);
const toolsResponse = await fetch(`${cachedEndpoint}/mcp/list_tools`);
if (!toolsResponse.ok) {
log(`Failed to fetch tools with status ${toolsResponse.status}`);
throw new Error("Unable to list tools");
}
const tools = await toolsResponse.json();
log(`Successfully fetched tools: ${JSON.stringify(tools)}`);
return {tools};
} catch (error) {
log("Error handling ListToolsRequestSchema request:", error);
throw error;
}
});
/**
* Handle calls to a specific tool by using the *cached* endpoint.
*/
async function handleToolCall(name: string, args: any): Promise<CallToolResult> {
log(`Handling tool call: name=${name}, args=${JSON.stringify(args)}`);
if (!cachedEndpoint) {
// If no cached endpoint, we can't proceed
throw new Error("No working IDE endpoint available.");
}
try {
log(`ENDPOINT: ${cachedEndpoint} | Tool name: ${name} | args: ${JSON.stringify(args)}`);
const response = await fetch(`${cachedEndpoint}/mcp/${name}`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(args),
});
if (!response.ok) {
log(`Response failed with status ${response.status} for tool ${name}`);
throw new Error(`Response failed: ${response.status}`);
}
// Parse the IDE's JSON response
const {status, error}: IDEResponse = await response.json();
log("Parsed response:", {status, error});
const isError = !!error;
const text = status ?? error;
log("Final response text:", text);
log("Is error:", isError);
return {
content: [{type: "text", text: text}],
isError,
};
} catch (error: any) {
log("Error in handleToolCall:", error);
return {
content: [{
type: "text",
text: error instanceof Error ? error.message : "Unknown error",
}],
isError: true,
};
}
}
// 1) Do an initial endpoint check (once at startup)
await updateIDEEndpoint();
/**
* Request handler for "CallToolRequestSchema"
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
log("Handling CallToolRequestSchema request:", request);
try {
const result = await handleToolCall(request.params.name, request.params.arguments ?? {});
log("Tool call handled successfully:", result);
return result;
} catch (error) {
log("Error handling CallToolRequestSchema request:", error);
throw error;
}
});
/**
* Starts the server, connects via stdio, and schedules endpoint checks.
*/
async function runServer() {
log("Initializing server...");
const transport = new StdioServerTransport();
try {
await server.connect(transport);
log("Server connected to transport.");
} catch (error) {
log("Error connecting server to transport:", error);
throw error;
}
// 2) Then check again every 10 seconds (in case IDE restarts or ports change)
setInterval(updateIDEEndpoint, 10_000);
log("Scheduled endpoint check every 10 seconds.");
log("JetBrains Proxy MCP Server running on stdio");
}
// Start the server
runServer().catch(error => {
log("Server failed to start:", error);
});