Skip to content
Open
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
34 changes: 21 additions & 13 deletions tegg/plugin/mcp-proxy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import cluster from 'node:cluster';
import querystring from 'node:querystring';
import { Readable } from 'node:stream';
import url from 'node:url';
import { fetch, Agent } from 'undici';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To improve performance and enable connection pooling, it is recommended to create a single Agent instance and reuse it across requests instead of instantiating a new one for every fetch call.

Suggested change
import { fetch, Agent } from 'undici';
import { fetch, Agent } from 'undici';
const proxyAgent = new Agent({
bodyTimeout: 0,
headersTimeout: 0,
});


import { MCPControllerRegister } from '@eggjs/controller-plugin/lib/impl/mcp/MCPControllerRegister';
import type { MCPControllerHook } from '@eggjs/controller-plugin/lib/impl/mcp/MCPControllerRegister';
Expand Down Expand Up @@ -343,11 +344,10 @@ export class MCPProxyApiClient extends APIClientBase {
ctx.req.headers['mcp-proxy-type'] = action;
ctx.req.headers['mcp-proxy-sessionid'] = sessionId;
const resp = await fetch(`http://localhost:${detail.port}/mcp/message?sessionId=${sessionId}`, {
// dispatcher: new Agent({
// connect: {
// socketPath,
// },
// }),
dispatcher: new Agent({
bodyTimeout: 0,
headersTimeout: 0,
}),
Comment on lines +347 to +350
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Use the shared proxyAgent instance to benefit from connection pooling and avoid the overhead of creating a new agent for each request.

            dispatcher: proxyAgent,

headers: ctx.req.headers as unknown as Record<string, string>,
body: body as string,
method: ctx.req.method,
Expand Down Expand Up @@ -389,11 +389,10 @@ export class MCPProxyApiClient extends APIClientBase {
ctx.req.headers['mcp-proxy-type'] = action;
ctx.req.headers['mcp-proxy-sessionid'] = sessionId;
const response = await fetch(`http://localhost:${detail.port}`, {
// dispatcher: new Agent({
// connect: {
// socketPath,
// },
// }),
dispatcher: new Agent({
bodyTimeout: 0,
headersTimeout: 0,
}),
Comment on lines +392 to +395
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Use the shared proxyAgent instance to benefit from connection pooling and avoid the overhead of creating a new agent for each request.

            dispatcher: proxyAgent,

headers: ctx.req.headers as unknown as Record<string, string>,
method: ctx.req.method,
...(ctx.req.method !== 'GET'
Expand All @@ -413,7 +412,14 @@ export class MCPProxyApiClient extends APIClientBase {
}
ctx.set(headers);
ctx.res.statusCode = response.status;
Readable.fromWeb(response.body! as any).pipe(ctx.res);
const readable = Readable.fromWeb(response.body!);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The use of the non-null assertion operator (!) on response.body could lead to a runtime error if the response has no body (e.g., a 204 No Content response). It is safer to check if the body exists before attempting to create a readable stream from it.

          if (!response.body) {
            ctx.res.end();
            break;
          }
          const readable = Readable.fromWeb(response.body);

readable.on('error', err => {
this.logger.error('[mcp-proxy] stream proxy error: %s', err.message);
if (!ctx.res.writableEnded) {
ctx.res.end();
}
});
readable.pipe(ctx.res);
Comment on lines +415 to +422
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against null response body before converting to Readable.

The non-null assertion response.body! is unsafe. If the upstream server returns a response without a body (e.g., certain error responses or 204 No Content), response.body will be null and Readable.fromWeb(null) will throw a runtime error.

🛡️ Proposed fix: Add null check
           ctx.res.statusCode = response.status;
+          if (!response.body) {
+            ctx.res.end();
+            break;
+          }
           const readable = Readable.fromWeb(response.body!);
           readable.on('error', err => {
             this.logger.error('[mcp-proxy] stream proxy error: %s', err.message);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/plugin/mcp-proxy/src/index.ts` around lines 415 - 422, The code uses
Readable.fromWeb(response.body!) which will throw if response.body is null;
update the proxy logic in the handler where Readable.fromWeb is called to first
check if response.body is null/undefined (and also check response.status ===
204/no-content), and if so skip creating the Readable, set appropriate
headers/status on ctx.res and call ctx.res.end(); otherwise create the Readable
from response.body, attach the 'error' handler and pipe to ctx.res as before
(referencing response, Readable.fromWeb, readable and ctx.res in the same
block).

break;
}
}
Expand Down Expand Up @@ -464,8 +470,10 @@ export class MCPProxyApiClient extends APIClientBase {
ctx.res.write('event: terminate');
} catch (error) {
ctx.res.statusCode = 500;
ctx.res.write(`see stream error ${error}`);
ctx.res.end();
if (!ctx.res.writableEnded) {
ctx.res.statusCode = 500;
ctx.res.end();
}
Comment on lines 472 to +476
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The status code is being set redundantly. Furthermore, attempting to set the status code after headers have been sent (which is likely in a streaming response) will result in errors or warnings. It is also good practice to log the error for observability.

        this.logger.error('[mcp-proxy] sse stream error: %s', error);
        if (!ctx.res.headersSent) {
          ctx.res.statusCode = 500;
        }
        if (!ctx.res.writableEnded) {
          ctx.res.end();
        }

}
Comment on lines 471 to 477
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove redundant statusCode assignment.

ctx.res.statusCode = 500 is set twice: unconditionally on line 472 and again inside the if block on line 474. If the response is already ended (writableEnded is true), setting statusCode on line 472 has no effect and may even throw in some Node.js versions. Move the assignment inside the guard.

🧹 Proposed fix
       } catch (error) {
-        ctx.res.statusCode = 500;
         if (!ctx.res.writableEnded) {
           ctx.res.statusCode = 500;
           ctx.res.end();
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
ctx.res.statusCode = 500;
ctx.res.write(`see stream error ${error}`);
ctx.res.end();
if (!ctx.res.writableEnded) {
ctx.res.statusCode = 500;
ctx.res.end();
}
}
} catch (error) {
if (!ctx.res.writableEnded) {
ctx.res.statusCode = 500;
ctx.res.end();
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/plugin/mcp-proxy/src/index.ts` around lines 471 - 477, The catch block
sets ctx.res.statusCode = 500 twice and may set status on a closed response;
remove the first unconditional assignment and move status setting inside the
writableEnded guard so only when !ctx.res.writableEnded you set
ctx.res.statusCode = 500 and then call ctx.res.end(); update the catch block
around the existing ctx and res usage to only change statusCode when the
response is writable (reference the catch block handling ctx.res and the
writableEnded check).

};
processStream();
Expand Down
Loading