@eggjs/mcp-proxy-plugin 0.0.0 → 4.0.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/agent.d.ts +10 -0
- package/dist/agent.js +13 -0
- package/dist/app/extend/agent.d.ts +6 -0
- package/dist/app/extend/agent.js +17 -0
- package/dist/app/extend/application.d.ts +6 -0
- package/dist/app/extend/application.js +16 -0
- package/dist/app.d.ts +11 -0
- package/dist/app.js +19 -0
- package/dist/config/config.default.d.ts +8 -0
- package/dist/config/config.default.js +7 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +367 -0
- package/dist/lib/MCPProxyDataClient.d.ts +17 -0
- package/dist/lib/MCPProxyDataClient.js +33 -0
- package/package.json +63 -10
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017-present Alibaba Group Holding Limited and other contributors.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/agent.d.ts
ADDED
package/dist/agent.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MCPProxyApiClient } from "../../index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/app/extend/agent.ts
|
|
4
|
+
const MCP_PROXY = Symbol("Application#mcpProxy");
|
|
5
|
+
var agent_default = { get mcpProxy() {
|
|
6
|
+
const self = this;
|
|
7
|
+
if (!self[MCP_PROXY]) self[MCP_PROXY] = new MCPProxyApiClient({
|
|
8
|
+
logger: this.logger,
|
|
9
|
+
messenger: this.messenger,
|
|
10
|
+
app: this,
|
|
11
|
+
isAgent: true
|
|
12
|
+
});
|
|
13
|
+
return self[MCP_PROXY];
|
|
14
|
+
} };
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
export { agent_default as default };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MCPProxyApiClient } from "../../index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/app/extend/application.ts
|
|
4
|
+
const MCP_PROXY = Symbol("Application#mcpProxy");
|
|
5
|
+
var application_default = { get mcpProxy() {
|
|
6
|
+
const self = this;
|
|
7
|
+
if (!self[MCP_PROXY]) self[MCP_PROXY] = new MCPProxyApiClient({
|
|
8
|
+
logger: this.logger,
|
|
9
|
+
messenger: this.messenger,
|
|
10
|
+
app: this
|
|
11
|
+
});
|
|
12
|
+
return self[MCP_PROXY];
|
|
13
|
+
} };
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { application_default as default };
|
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MCPProxyHook } from "./index.js";
|
|
2
|
+
import { MCPControllerRegister } from "@eggjs/controller-plugin/lib/impl/mcp/MCPControllerRegister";
|
|
3
|
+
|
|
4
|
+
//#region src/app.ts
|
|
5
|
+
var AppHook = class {
|
|
6
|
+
agent;
|
|
7
|
+
constructor(agent) {
|
|
8
|
+
this.agent = agent;
|
|
9
|
+
}
|
|
10
|
+
configWillLoad() {
|
|
11
|
+
MCPControllerRegister.addHook(MCPProxyHook);
|
|
12
|
+
}
|
|
13
|
+
async didLoad() {
|
|
14
|
+
if (this.agent.mcpProxy) await this.agent.mcpProxy?.ready();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { AppHook as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MCPProxyDataClient } from "./lib/MCPProxyDataClient.js";
|
|
2
|
+
import { MCPControllerHook } from "@eggjs/controller-plugin/lib/impl/mcp/MCPControllerRegister";
|
|
3
|
+
import { MCPProtocols } from "@eggjs/tegg-types";
|
|
4
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
import { APIClientBase } from "cluster-client";
|
|
7
|
+
import { Application, Context, EggLogger } from "egg";
|
|
8
|
+
|
|
9
|
+
//#region src/index.d.ts
|
|
10
|
+
interface MCPProxyPayload {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
message: unknown;
|
|
13
|
+
}
|
|
14
|
+
interface ProxyMessageOptions {
|
|
15
|
+
detail: ClientDetail;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
type: MCPProtocols;
|
|
18
|
+
}
|
|
19
|
+
interface ClientDetail {
|
|
20
|
+
pid: number;
|
|
21
|
+
port: number;
|
|
22
|
+
}
|
|
23
|
+
declare const MCPProxyHook: MCPControllerHook;
|
|
24
|
+
declare class MCPProxyApiClient extends APIClientBase {
|
|
25
|
+
private _client;
|
|
26
|
+
private logger;
|
|
27
|
+
private proxyHandlerMap;
|
|
28
|
+
private port;
|
|
29
|
+
private app;
|
|
30
|
+
private isAgent;
|
|
31
|
+
constructor(options: {
|
|
32
|
+
logger: EggLogger;
|
|
33
|
+
messenger: any;
|
|
34
|
+
app: Application;
|
|
35
|
+
isAgent?: boolean;
|
|
36
|
+
});
|
|
37
|
+
_init(): Promise<void>;
|
|
38
|
+
setProxyHandler(type: MCPProtocols, handler: StreamableHTTPServerTransport["handleRequest"] | SSEServerTransport["handlePostMessage"]): void;
|
|
39
|
+
registerClient(sessionId: string, pid: number): Promise<void>;
|
|
40
|
+
unregisterClient(sessionId: string): Promise<void>;
|
|
41
|
+
getClient(sessionId: string): Promise<ClientDetail | undefined>;
|
|
42
|
+
proxyMessage(ctx: Context, options: ProxyMessageOptions): Promise<void>;
|
|
43
|
+
handleSseStream(ctx: Context, stream: ReadableStream<any>): void;
|
|
44
|
+
get delegates(): Record<string, string>;
|
|
45
|
+
get DataClient(): typeof MCPProxyDataClient;
|
|
46
|
+
get clusterOptions(): {
|
|
47
|
+
name: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { ClientDetail, MCPProxyApiClient, MCPProxyHook, MCPProxyPayload, ProxyMessageOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { MCPProxyDataClient } from "./lib/MCPProxyDataClient.js";
|
|
2
|
+
import { MCPControllerRegister } from "@eggjs/controller-plugin/lib/impl/mcp/MCPControllerRegister";
|
|
3
|
+
import http from "http";
|
|
4
|
+
import cluster from "node:cluster";
|
|
5
|
+
import querystring from "node:querystring";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import url from "node:url";
|
|
8
|
+
import { MCPProtocols } from "@eggjs/tegg-types";
|
|
9
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
10
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11
|
+
import awaitEvent from "await-event";
|
|
12
|
+
import { APIClientBase } from "cluster-client";
|
|
13
|
+
import contentType from "content-type";
|
|
14
|
+
import { EventSourceParserStream } from "eventsource-parser/stream";
|
|
15
|
+
import compose from "koa-compose";
|
|
16
|
+
import getRawBody from "raw-body";
|
|
17
|
+
|
|
18
|
+
//#region src/index.ts
|
|
19
|
+
const MAXIMUM_MESSAGE_SIZE = "4mb";
|
|
20
|
+
const IGNORE_HEADERS = [
|
|
21
|
+
"connection",
|
|
22
|
+
"upgrade",
|
|
23
|
+
"keep-alive",
|
|
24
|
+
"proxy-connection",
|
|
25
|
+
"te",
|
|
26
|
+
"trailer",
|
|
27
|
+
"transfer-encoding"
|
|
28
|
+
];
|
|
29
|
+
const MCPProxyHook = {
|
|
30
|
+
async preSSEInitHandle(ctx, transport, self) {
|
|
31
|
+
const id = transport.sessionId;
|
|
32
|
+
await self.app.mcpProxy.registerClient(id, process.pid);
|
|
33
|
+
self.app.mcpProxy.setProxyHandler(MCPProtocols.SSE, async (req, res) => {
|
|
34
|
+
const sessionId = querystring.parse(url.parse(req.url).query ?? "").sessionId;
|
|
35
|
+
const ctx$1 = self.app.createContext(req, res);
|
|
36
|
+
if (MCPControllerRegister.hooks.length > 0) for (const hook of MCPControllerRegister.hooks) await hook.preProxy?.(ctx$1, req, res);
|
|
37
|
+
let transport$1;
|
|
38
|
+
const existingTransport = self.transports[sessionId];
|
|
39
|
+
if (existingTransport instanceof SSEServerTransport) transport$1 = existingTransport;
|
|
40
|
+
else {
|
|
41
|
+
res.writeHead(400).end(JSON.stringify({
|
|
42
|
+
jsonrpc: "2.0",
|
|
43
|
+
error: {
|
|
44
|
+
code: -32e3,
|
|
45
|
+
message: "Bad Request: Session exists but uses a different transport protocol"
|
|
46
|
+
},
|
|
47
|
+
id: null
|
|
48
|
+
}));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (transport$1) try {
|
|
52
|
+
await self.transports[sessionId].handlePostMessage(req, res);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
self.app.logger.error("Error handling MCP message", error);
|
|
55
|
+
if (!ctx$1.res.headersSent) {
|
|
56
|
+
ctx$1.status = 500;
|
|
57
|
+
ctx$1.body = {
|
|
58
|
+
jsonrpc: "2.0",
|
|
59
|
+
error: {
|
|
60
|
+
code: -32603,
|
|
61
|
+
message: `Internal error: ${error.message}`
|
|
62
|
+
},
|
|
63
|
+
id: null
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else res.writeHead(404).end(JSON.stringify({
|
|
68
|
+
jsonrpc: "2.0",
|
|
69
|
+
error: {
|
|
70
|
+
code: -32602,
|
|
71
|
+
message: "Bad Request: No transport found for sessionId"
|
|
72
|
+
},
|
|
73
|
+
id: null
|
|
74
|
+
}));
|
|
75
|
+
});
|
|
76
|
+
ctx.res.once("close", () => {
|
|
77
|
+
delete self.transports[id];
|
|
78
|
+
const connection = self.sseConnections.get(id);
|
|
79
|
+
if (connection) {
|
|
80
|
+
clearInterval(connection.intervalId);
|
|
81
|
+
self.sseConnections.delete(id);
|
|
82
|
+
}
|
|
83
|
+
self.app.mcpProxy.unregisterClient(id);
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
async onStreamSessionInitialized(_ctx, transport, server, self) {
|
|
87
|
+
const sessionId = transport.sessionId;
|
|
88
|
+
self.streamTransports[sessionId] = transport;
|
|
89
|
+
self.mcpServerMap[sessionId] = server;
|
|
90
|
+
self.app.mcpProxy.setProxyHandler(MCPProtocols.STREAM, async (req, res) => {
|
|
91
|
+
let mw = self.app.middleware.teggCtxLifecycleMiddleware();
|
|
92
|
+
if (self.globalMiddlewares) mw = compose([mw, self.globalMiddlewares]);
|
|
93
|
+
const ctx = self.app.createContext(req, res);
|
|
94
|
+
if (MCPControllerRegister.hooks.length > 0) for (const hook of MCPControllerRegister.hooks) await hook.preProxy?.(ctx, req, res);
|
|
95
|
+
const sessionId$1 = req.headers["mcp-session-id"];
|
|
96
|
+
if (!sessionId$1) {
|
|
97
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
98
|
+
res.end(JSON.stringify({
|
|
99
|
+
jsonrpc: "2.0",
|
|
100
|
+
error: {
|
|
101
|
+
code: -32603,
|
|
102
|
+
message: "session id not have and run in proxy"
|
|
103
|
+
},
|
|
104
|
+
id: null
|
|
105
|
+
}));
|
|
106
|
+
} else {
|
|
107
|
+
let transport$1;
|
|
108
|
+
const existingTransport = self.streamTransports[sessionId$1];
|
|
109
|
+
if (existingTransport instanceof StreamableHTTPServerTransport) transport$1 = existingTransport;
|
|
110
|
+
else {
|
|
111
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
112
|
+
res.end(JSON.stringify({
|
|
113
|
+
jsonrpc: "2.0",
|
|
114
|
+
error: {
|
|
115
|
+
code: -32e3,
|
|
116
|
+
message: "Bad Request: Session exists but uses a different transport protocol"
|
|
117
|
+
},
|
|
118
|
+
id: null
|
|
119
|
+
}));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (transport$1) await self.app.ctxStorage.run(ctx, async () => {
|
|
123
|
+
await mw(ctx, async () => {
|
|
124
|
+
await transport$1.handleRequest(ctx.req, ctx.res);
|
|
125
|
+
await awaitEvent(ctx.res, "close");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
else {
|
|
129
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
130
|
+
res.end(JSON.stringify({
|
|
131
|
+
jsonrpc: "2.0",
|
|
132
|
+
error: {
|
|
133
|
+
code: -32602,
|
|
134
|
+
message: "Bad Request: No transport found for sessionId"
|
|
135
|
+
},
|
|
136
|
+
id: null
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
await self.app.mcpProxy.registerClient(sessionId, process.pid);
|
|
142
|
+
transport.onclose = async () => {
|
|
143
|
+
const sid = transport.sessionId;
|
|
144
|
+
if (sid && self.streamTransports[sid]) {
|
|
145
|
+
delete self.streamTransports[sid];
|
|
146
|
+
delete self.mcpServerMap[sid];
|
|
147
|
+
}
|
|
148
|
+
await self.app.mcpProxy.unregisterClient(sid);
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
async checkAndRunProxy(ctx, type, sessionId) {
|
|
152
|
+
const detail = await ctx.app.mcpProxy.getClient(sessionId);
|
|
153
|
+
if (detail?.pid !== process.pid) {
|
|
154
|
+
await ctx.app.mcpProxy.proxyMessage(ctx, {
|
|
155
|
+
detail,
|
|
156
|
+
sessionId,
|
|
157
|
+
type
|
|
158
|
+
});
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
var MCPProxyApiClient = class extends APIClientBase {
|
|
165
|
+
_client;
|
|
166
|
+
logger;
|
|
167
|
+
proxyHandlerMap = {};
|
|
168
|
+
port;
|
|
169
|
+
app;
|
|
170
|
+
isAgent;
|
|
171
|
+
constructor(options) {
|
|
172
|
+
super(Object.assign({}, options, { initMethod: "_init" }));
|
|
173
|
+
this.logger = options.logger;
|
|
174
|
+
this.port = 0;
|
|
175
|
+
this.app = options.app;
|
|
176
|
+
this.isAgent = !!options.isAgent;
|
|
177
|
+
}
|
|
178
|
+
async _init() {
|
|
179
|
+
if (!this.isAgent) {
|
|
180
|
+
const validProxyActions = new Set([
|
|
181
|
+
"MCP_STDIO_PROXY",
|
|
182
|
+
"MCP_SEE_PROXY",
|
|
183
|
+
"MCP_STREAM_PROXY"
|
|
184
|
+
]);
|
|
185
|
+
const server = http.createServer(async (req, res) => {
|
|
186
|
+
const type = req.headers["mcp-proxy-type"];
|
|
187
|
+
if (!type || !validProxyActions.has(type)) {
|
|
188
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
189
|
+
res.end(JSON.stringify({
|
|
190
|
+
jsonrpc: "2.0",
|
|
191
|
+
error: {
|
|
192
|
+
code: -32600,
|
|
193
|
+
message: "Invalid proxy type"
|
|
194
|
+
},
|
|
195
|
+
id: null
|
|
196
|
+
}));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
await this.proxyHandlerMap[type]?.(req, res);
|
|
200
|
+
});
|
|
201
|
+
this.port = this.app.config.mcp?.proxyPort + (cluster.worker?.id ?? 0);
|
|
202
|
+
await new Promise((resolve) => server.listen(this.port, () => {
|
|
203
|
+
resolve(null);
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
setProxyHandler(type, handler) {
|
|
208
|
+
let action;
|
|
209
|
+
switch (type) {
|
|
210
|
+
case MCPProtocols.SSE:
|
|
211
|
+
action = "MCP_SEE_PROXY";
|
|
212
|
+
break;
|
|
213
|
+
case MCPProtocols.STDIO:
|
|
214
|
+
action = "MCP_STDIO_PROXY";
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
action = "MCP_STREAM_PROXY";
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
this.proxyHandlerMap[action] = handler;
|
|
221
|
+
}
|
|
222
|
+
async registerClient(sessionId, pid) {
|
|
223
|
+
await this._client.registerClient(sessionId, {
|
|
224
|
+
pid,
|
|
225
|
+
port: this.port
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async unregisterClient(sessionId) {
|
|
229
|
+
await this._client.unregisterClient(sessionId);
|
|
230
|
+
}
|
|
231
|
+
async getClient(sessionId) {
|
|
232
|
+
return this._client.getClient(sessionId);
|
|
233
|
+
}
|
|
234
|
+
async proxyMessage(ctx, options) {
|
|
235
|
+
let body;
|
|
236
|
+
const { detail, sessionId, type } = options;
|
|
237
|
+
try {
|
|
238
|
+
let encoding = "utf-8";
|
|
239
|
+
if (ctx.req.headers["content-type"]) {
|
|
240
|
+
const ct = contentType.parse(ctx.req.headers["content-type"] ?? "");
|
|
241
|
+
if (ct.type !== "application/json") throw new Error(`Unsupported content-type: ${ct}`);
|
|
242
|
+
encoding = ct.parameters.charset;
|
|
243
|
+
}
|
|
244
|
+
body = await getRawBody(ctx.req, {
|
|
245
|
+
limit: MAXIMUM_MESSAGE_SIZE,
|
|
246
|
+
encoding
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.logger.error(error);
|
|
250
|
+
ctx.res.writeHead(400).end(JSON.stringify({
|
|
251
|
+
jsonrpc: "2.0",
|
|
252
|
+
error: {
|
|
253
|
+
code: -32602,
|
|
254
|
+
message: `Bad Request: ${String(error)}`
|
|
255
|
+
},
|
|
256
|
+
id: null
|
|
257
|
+
}));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
let action;
|
|
262
|
+
switch (type) {
|
|
263
|
+
case "SSE": {
|
|
264
|
+
action = "MCP_SEE_PROXY";
|
|
265
|
+
ctx.req.headers["mcp-proxy-type"] = action;
|
|
266
|
+
ctx.req.headers["mcp-proxy-sessionid"] = sessionId;
|
|
267
|
+
const resp = await fetch(`http://localhost:${detail.port}/mcp/message?sessionId=${sessionId}`, {
|
|
268
|
+
headers: ctx.req.headers,
|
|
269
|
+
body,
|
|
270
|
+
method: ctx.req.method
|
|
271
|
+
});
|
|
272
|
+
const headers = { "mcp-proxy-arg": encodeURIComponent(body.toString()) };
|
|
273
|
+
for (const [key, value] of resp.headers.entries()) {
|
|
274
|
+
if (IGNORE_HEADERS.includes(key)) continue;
|
|
275
|
+
headers[key] = value;
|
|
276
|
+
}
|
|
277
|
+
ctx.set(headers);
|
|
278
|
+
ctx.res.statusCode = resp.status;
|
|
279
|
+
ctx.res.statusMessage = resp.statusText;
|
|
280
|
+
ctx.body = await resp.text();
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "STDIO":
|
|
284
|
+
action = "MCP_STDIO_PROXY";
|
|
285
|
+
ctx.req.headers["mcp-proxy-type"] = action;
|
|
286
|
+
ctx.req.headers["mcp-proxy-sessionid"] = sessionId;
|
|
287
|
+
ctx.res.writeHead(400).end(JSON.stringify({
|
|
288
|
+
jsonrpc: "2.0",
|
|
289
|
+
error: {
|
|
290
|
+
code: -32602,
|
|
291
|
+
message: "Bad Request: STDIO IS NOT IMPL"
|
|
292
|
+
},
|
|
293
|
+
id: null
|
|
294
|
+
}));
|
|
295
|
+
break;
|
|
296
|
+
default: {
|
|
297
|
+
action = "MCP_STREAM_PROXY";
|
|
298
|
+
ctx.respond = false;
|
|
299
|
+
ctx.req.headers["mcp-proxy-type"] = action;
|
|
300
|
+
ctx.req.headers["mcp-proxy-sessionid"] = sessionId;
|
|
301
|
+
const response = await fetch(`http://localhost:${detail.port}`, {
|
|
302
|
+
headers: ctx.req.headers,
|
|
303
|
+
method: ctx.req.method,
|
|
304
|
+
...ctx.req.method !== "GET" ? { body } : {}
|
|
305
|
+
});
|
|
306
|
+
const headers = { "mcp-proxy-arg": encodeURIComponent(body.toString()) };
|
|
307
|
+
for (const [key, value] of response.headers.entries()) {
|
|
308
|
+
if (IGNORE_HEADERS.includes(key)) continue;
|
|
309
|
+
headers[key] = value;
|
|
310
|
+
}
|
|
311
|
+
ctx.set(headers);
|
|
312
|
+
ctx.res.statusCode = response.status;
|
|
313
|
+
Readable.fromWeb(response.body).pipe(ctx.res);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
this.logger.error(error);
|
|
319
|
+
ctx.res.writeHead(500, { "content-type": "application/json" }).end(JSON.stringify({
|
|
320
|
+
jsonrpc: "2.0",
|
|
321
|
+
error: {
|
|
322
|
+
code: -32603,
|
|
323
|
+
message: "Internal error"
|
|
324
|
+
},
|
|
325
|
+
id: null
|
|
326
|
+
}));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
handleSseStream(ctx, stream) {
|
|
331
|
+
const processStream = async () => {
|
|
332
|
+
try {
|
|
333
|
+
const reader = stream.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream()).getReader();
|
|
334
|
+
while (true) {
|
|
335
|
+
const { value: event, done } = await reader.read();
|
|
336
|
+
if (done) break;
|
|
337
|
+
let eventData = "event: message\n";
|
|
338
|
+
if (event.id) eventData += `id: ${event.id}\n`;
|
|
339
|
+
eventData += `data: ${JSON.stringify(event.data)}\n\n`;
|
|
340
|
+
ctx.res.write(eventData);
|
|
341
|
+
}
|
|
342
|
+
ctx.res.write("event: terminate");
|
|
343
|
+
} catch (error) {
|
|
344
|
+
ctx.res.statusCode = 500;
|
|
345
|
+
ctx.res.write(`see stream error ${error}`);
|
|
346
|
+
ctx.res.end();
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
processStream();
|
|
350
|
+
}
|
|
351
|
+
get delegates() {
|
|
352
|
+
return {
|
|
353
|
+
registerClient: "invoke",
|
|
354
|
+
unregisterClient: "invoke",
|
|
355
|
+
getClient: "invoke"
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
get DataClient() {
|
|
359
|
+
return MCPProxyDataClient;
|
|
360
|
+
}
|
|
361
|
+
get clusterOptions() {
|
|
362
|
+
return { name: "MCPProxy" };
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
//#endregion
|
|
367
|
+
export { MCPProxyApiClient, MCPProxyHook };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Base } from "sdk-base";
|
|
2
|
+
import { EggLogger } from "egg";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/MCPProxyDataClient.d.ts
|
|
5
|
+
declare class MCPProxyDataClient extends Base {
|
|
6
|
+
private readonly clients;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
constructor(options: {
|
|
9
|
+
logger: EggLogger;
|
|
10
|
+
});
|
|
11
|
+
_init(): Promise<void>;
|
|
12
|
+
registerClient(sessionId: string, pid: number): Promise<void>;
|
|
13
|
+
getClient(sessionId: string): Promise<number | undefined>;
|
|
14
|
+
unregisterClient(sessionId: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { MCPProxyDataClient };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Base } from "sdk-base";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/MCPProxyDataClient.ts
|
|
4
|
+
var MCPProxyDataClient = class extends Base {
|
|
5
|
+
clients;
|
|
6
|
+
logger;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
const superOptions = Object.assign({}, { initMethod: "_init" });
|
|
9
|
+
super(superOptions);
|
|
10
|
+
this.clients = /* @__PURE__ */ new Map();
|
|
11
|
+
this.logger = options.logger;
|
|
12
|
+
}
|
|
13
|
+
async _init() {}
|
|
14
|
+
async registerClient(sessionId, pid) {
|
|
15
|
+
if (this.clients.has(sessionId)) {
|
|
16
|
+
const oldPid = this.clients.get(sessionId);
|
|
17
|
+
this.logger.info("[MCPClientManager] duplicate register client %s new pid %s old pid", sessionId, pid, oldPid);
|
|
18
|
+
this.clients.set(sessionId, pid);
|
|
19
|
+
} else {
|
|
20
|
+
this.logger.info("[MCPClientManager] register client %s pid %s", sessionId, pid);
|
|
21
|
+
this.clients.set(sessionId, pid);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async getClient(sessionId) {
|
|
25
|
+
return this.clients.get(sessionId);
|
|
26
|
+
}
|
|
27
|
+
async unregisterClient(sessionId) {
|
|
28
|
+
this.clients.delete(sessionId);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { MCPProxyDataClient };
|
package/package.json
CHANGED
|
@@ -1,31 +1,84 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eggjs/mcp-proxy-plugin",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.0.2-beta.0",
|
|
4
|
+
"description": "tegg mcp proxy plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"egg",
|
|
7
|
-
"
|
|
7
|
+
"mcp",
|
|
8
|
+
"plugin",
|
|
9
|
+
"tegg",
|
|
10
|
+
"typescript"
|
|
8
11
|
],
|
|
9
|
-
"homepage": "https://github.com/eggjs/egg/tree/next/
|
|
12
|
+
"homepage": "https://github.com/eggjs/egg/tree/next/tegg/plugin/mcp-proxy",
|
|
10
13
|
"bugs": {
|
|
11
14
|
"url": "https://github.com/eggjs/egg/issues"
|
|
12
15
|
},
|
|
13
|
-
"license": "MIT",
|
|
14
|
-
"author": "fengmk2 <fengmk2@gmail.com> (https://github.com/fengmk2)",
|
|
15
16
|
"repository": {
|
|
16
17
|
"type": "git",
|
|
17
18
|
"url": "git+https://github.com/eggjs/egg.git",
|
|
18
|
-
"directory": "
|
|
19
|
+
"directory": "tegg/plugin/mcp-proxy"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./dist/index.js",
|
|
30
|
+
"./agent": "./dist/agent.js",
|
|
31
|
+
"./app": "./dist/app.js",
|
|
32
|
+
"./app/extend/agent": "./dist/app/extend/agent.js",
|
|
33
|
+
"./app/extend/application": "./dist/app/extend/application.js",
|
|
34
|
+
"./config/config.default": "./dist/config/config.default.js",
|
|
35
|
+
"./lib/MCPProxyDataClient": "./dist/lib/MCPProxyDataClient.js",
|
|
36
|
+
"./package.json": "./package.json"
|
|
19
37
|
},
|
|
20
38
|
"publishConfig": {
|
|
21
39
|
"access": "public"
|
|
22
40
|
},
|
|
23
41
|
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
43
|
+
"await-event": "2",
|
|
44
|
+
"cluster-client": "^3.7.0",
|
|
45
|
+
"content-type": "^1.0.5",
|
|
46
|
+
"eventsource-parser": "^3.0.1",
|
|
47
|
+
"koa-compose": "^4.1.0",
|
|
48
|
+
"raw-body": "^2.5.2",
|
|
49
|
+
"sdk-base": "^5.0.1",
|
|
50
|
+
"@eggjs/controller-plugin": "4.0.2-beta.0",
|
|
51
|
+
"@eggjs/tegg-types": "4.0.2-beta.0"
|
|
24
52
|
},
|
|
25
53
|
"devDependencies": {
|
|
54
|
+
"@types/node": "^24.10.2",
|
|
55
|
+
"eventsource": "^3.0.5",
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"@eggjs/aop-runtime": "4.0.2-beta.0",
|
|
58
|
+
"@eggjs/metadata": "4.0.2-beta.0",
|
|
59
|
+
"@eggjs/mock": "7.0.2-beta.0",
|
|
60
|
+
"@eggjs/tegg": "4.0.2-beta.0",
|
|
61
|
+
"@eggjs/tegg-config": "4.0.2-beta.0",
|
|
62
|
+
"@eggjs/tegg-plugin": "4.0.2-beta.0",
|
|
63
|
+
"egg": "4.1.2-beta.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"egg": "4.1.2-beta.0",
|
|
67
|
+
"@eggjs/tegg-plugin": "4.0.2-beta.0"
|
|
26
68
|
},
|
|
27
69
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
70
|
+
"node": ">=22.18.0"
|
|
71
|
+
},
|
|
72
|
+
"eggModule": {
|
|
73
|
+
"name": "mcpProxy"
|
|
74
|
+
},
|
|
75
|
+
"eggPlugin": {
|
|
76
|
+
"name": "mcpProxy",
|
|
77
|
+
"dependencies": [
|
|
78
|
+
"tegg"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
"scripts": {
|
|
82
|
+
"typecheck": "tsgo --noEmit"
|
|
29
83
|
}
|
|
30
|
-
}
|
|
31
|
-
|
|
84
|
+
}
|