@deadragdoll/tellymcp 0.0.1
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/.env.example.client +93 -0
- package/.env.example.gateway +120 -0
- package/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README-ru.md +338 -0
- package/README.md +1262 -0
- package/STANDALONE-ru.md +266 -0
- package/STANDALONE.md +266 -0
- package/TOOLS.md +1296 -0
- package/config/templates/env.both.template +83 -0
- package/config/templates/env.client.template +60 -0
- package/config/templates/env.gateway.template +82 -0
- package/dist/cli.js +636 -0
- package/dist/index.js +17 -0
- package/dist/lib/logfeed/store.js +52 -0
- package/dist/lib/middlewares/tracer.js +172 -0
- package/dist/lib/mixins/db.js +267 -0
- package/dist/lib/mixins/logfeed.js +34 -0
- package/dist/lib/mixins/session.errors.js +142 -0
- package/dist/lib/moleculer.js +2 -0
- package/dist/lib/trace.js +147 -0
- package/dist/lib/traceContext.js +116 -0
- package/dist/moleculer.config.js +274 -0
- package/dist/services/features/telegram-mcp/approval.service.js +33 -0
- package/dist/services/features/telegram-mcp/browser.service.js +42 -0
- package/dist/services/features/telegram-mcp/collaboration.service.js +53 -0
- package/dist/services/features/telegram-mcp/ensuredb.service.js +337 -0
- package/dist/services/features/telegram-mcp/gateway-delivery.service.js +378 -0
- package/dist/services/features/telegram-mcp/gateway-loopback.js +10 -0
- package/dist/services/features/telegram-mcp/gateway-rmq.service.js +294 -0
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +1463 -0
- package/dist/services/features/telegram-mcp/gateway.service.js +1141 -0
- package/dist/services/features/telegram-mcp/inbox.service.js +33 -0
- package/dist/services/features/telegram-mcp/mcp-http.service.js +76 -0
- package/dist/services/features/telegram-mcp/mcp-server.service.js +127 -0
- package/dist/services/features/telegram-mcp/notify.service.js +33 -0
- package/dist/services/features/telegram-mcp/pair.service.js +33 -0
- package/dist/services/features/telegram-mcp/runtime.service.js +36 -0
- package/dist/services/features/telegram-mcp/session-context.service.js +33 -0
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +103 -0
- package/dist/services/features/telegram-mcp/src/app/config/env.js +317 -0
- package/dist/services/features/telegram-mcp/src/app/http.js +774 -0
- package/dist/services/features/telegram-mcp/src/app/index.js +2 -0
- package/dist/services/features/telegram-mcp/src/app/providers/mcp/server.js +13 -0
- package/dist/services/features/telegram-mcp/src/app/providers/redis/client.js +18 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +740 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +267 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/relay.js +69 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/tmux.js +9 -0
- package/dist/services/features/telegram-mcp/src/entities/auth/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/entities/browser/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/entities/collaboration/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/entities/inbox/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +545 -0
- package/dist/services/features/telegram-mcp/src/entities/request/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/entities/session/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/features/ask-user/model/askUserTelegram.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClearLogsTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserCloseTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserComputedStyleTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserConsoleTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserErrorsTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserNetworkFailuresTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserOpenTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserReloadTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +689 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserWaitForTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserWaitForUrlTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/backend.js +2 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +26 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/localCollaborationBackend.js +390 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +102 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerNoteTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayCollaborationBackend.js +69 -0
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +657 -0
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayReplyResolution.js +17 -0
- package/dist/services/features/telegram-mcp/src/features/inbox/model/deleteTelegramInboxMessageTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/inbox/model/getTelegramInboxCountTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/inbox/model/getTelegramInboxTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/inbox/model/inboxService.js +77 -0
- package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +93 -0
- package/dist/services/features/telegram-mcp/src/features/notify/model/notifyTelegramTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/pair-session/model/clearSessionPairingTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/pair-session/model/createSessionPairCodeTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/pair-session/model/generatePairCode.js +202 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/clearSessionContextTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/getSessionContextTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/getTmuxTargetTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/renameSessionTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/sessionContextService.js +409 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/setSessionContextTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/setTmuxTargetTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/tools-sync/model/refreshToolsMarkdownService.js +123 -0
- package/dist/services/features/telegram-mcp/src/features/tools-sync/model/refreshToolsMarkdownTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/processes/human-approval/model/orchestrator.js +243 -0
- package/dist/services/features/telegram-mcp/src/shared/api/storage/contract.js +2 -0
- package/dist/services/features/telegram-mcp/src/shared/api/tool-registry/registry.js +8 -0
- package/dist/services/features/telegram-mcp/src/shared/api/tool-registry/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/shared/api/transport/contract.js +2 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/object-storage/minioExchangeStore.js +86 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +436 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/collabSemantics.js +21 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/collabUi.js +87 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/messageFormat.js +60 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/proxyFetch.js +46 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +6534 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/tmux/client.js +280 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/ids/ids.js +34 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/logger/logger.js +68 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/project-identity/projectIdentity.js +223 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/redact-secrets/redactSecrets.js +22 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/truncate/truncate.js +12 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/version/versionHandshake.js +124 -0
- package/dist/services/features/telegram-mcp/src/shared/types/common.js +2 -0
- package/dist/services/features/telegram-mcp/standalone-http.service.js +113 -0
- package/dist/services/features/telegram-mcp/tools-sync.service.js +33 -0
- package/package.json +110 -0
- package/scripts/postinstall.js +60 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMcpHttpHandler = createMcpHttpHandler;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
6
|
+
const auth_1 = require("./webapp/auth");
|
|
7
|
+
const relay_1 = require("./webapp/relay");
|
|
8
|
+
const assets_1 = require("./webapp/assets");
|
|
9
|
+
const tmux_1 = require("./webapp/tmux");
|
|
10
|
+
function formatTmuxHttpError(error, fallback) {
|
|
11
|
+
if ((0, tmux_1.isTmuxUnavailableError)(error)) {
|
|
12
|
+
return "tmux is unavailable";
|
|
13
|
+
}
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
function isInitializeRequest(body) {
|
|
17
|
+
if (!body || typeof body !== "object") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const method = Reflect.get(body, "method");
|
|
21
|
+
return method === "initialize";
|
|
22
|
+
}
|
|
23
|
+
function readHeader(req, headerName) {
|
|
24
|
+
const value = req.headers[headerName];
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return value[0];
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
async function readJsonBody(req) {
|
|
31
|
+
const knownParams = req.$params;
|
|
32
|
+
if (knownParams &&
|
|
33
|
+
typeof knownParams === "object" &&
|
|
34
|
+
Object.keys(knownParams).length > 0) {
|
|
35
|
+
return knownParams;
|
|
36
|
+
}
|
|
37
|
+
const knownBody = req.body;
|
|
38
|
+
if (knownBody !== undefined) {
|
|
39
|
+
return knownBody;
|
|
40
|
+
}
|
|
41
|
+
const chunks = [];
|
|
42
|
+
for await (const chunk of req) {
|
|
43
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
44
|
+
}
|
|
45
|
+
if (chunks.length === 0) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
49
|
+
if (!raw) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return JSON.parse(raw);
|
|
53
|
+
}
|
|
54
|
+
function writeJson(res, statusCode, payload) {
|
|
55
|
+
const body = JSON.stringify(payload);
|
|
56
|
+
res.statusCode = statusCode;
|
|
57
|
+
res.setHeader("content-type", "application/json");
|
|
58
|
+
res.end(body);
|
|
59
|
+
}
|
|
60
|
+
function writeText(res, statusCode, message) {
|
|
61
|
+
res.statusCode = statusCode;
|
|
62
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
63
|
+
res.end(message);
|
|
64
|
+
}
|
|
65
|
+
function writeHtml(res, statusCode, html) {
|
|
66
|
+
res.statusCode = statusCode;
|
|
67
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
68
|
+
res.end(html);
|
|
69
|
+
}
|
|
70
|
+
function writeJavaScript(res, statusCode, source) {
|
|
71
|
+
res.statusCode = statusCode;
|
|
72
|
+
res.setHeader("content-type", "application/javascript; charset=utf-8");
|
|
73
|
+
res.end(source);
|
|
74
|
+
}
|
|
75
|
+
function writeCss(res, statusCode, source) {
|
|
76
|
+
res.statusCode = statusCode;
|
|
77
|
+
res.setHeader("content-type", "text/css; charset=utf-8");
|
|
78
|
+
res.end(source);
|
|
79
|
+
}
|
|
80
|
+
function isAuthorized(req, expectedBearerToken) {
|
|
81
|
+
if (!expectedBearerToken) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
const header = readHeader(req, "authorization");
|
|
85
|
+
return header === `Bearer ${expectedBearerToken}`;
|
|
86
|
+
}
|
|
87
|
+
function readBearerToken(req) {
|
|
88
|
+
const header = readHeader(req, "authorization");
|
|
89
|
+
if (!header?.startsWith("Bearer ")) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return header.slice("Bearer ".length).trim() || null;
|
|
93
|
+
}
|
|
94
|
+
function isDuplicateSseStreamError(error) {
|
|
95
|
+
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
96
|
+
return message.includes("Only one SSE stream is allowed per session");
|
|
97
|
+
}
|
|
98
|
+
function normalizePrefixedPathname(pathname) {
|
|
99
|
+
const rootPrefix = process.env.ROOT_PREFIX || "/api";
|
|
100
|
+
const normalizedRootPrefix = rootPrefix !== "/" ? rootPrefix.replace(/\/+$/u, "") : "/";
|
|
101
|
+
if (normalizedRootPrefix === "/" ||
|
|
102
|
+
!pathname.startsWith(normalizedRootPrefix)) {
|
|
103
|
+
return pathname || "/";
|
|
104
|
+
}
|
|
105
|
+
const stripped = pathname.slice(normalizedRootPrefix.length);
|
|
106
|
+
return stripped.startsWith("/") ? stripped : `/${stripped || ""}`;
|
|
107
|
+
}
|
|
108
|
+
function normalizeBasePath(value) {
|
|
109
|
+
const trimmed = value.trim().replace(/\/+$/u, "");
|
|
110
|
+
if (!trimmed) {
|
|
111
|
+
return "/";
|
|
112
|
+
}
|
|
113
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
114
|
+
}
|
|
115
|
+
function isRelayTmuxUnavailableMessage(error) {
|
|
116
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
117
|
+
return text.includes("tmux is unavailable");
|
|
118
|
+
}
|
|
119
|
+
function createMcpHttpHandler(runtime, input) {
|
|
120
|
+
const transports = new Map();
|
|
121
|
+
const webAppSessions = new auth_1.WebAppSessionRegistry();
|
|
122
|
+
const webAppBasePath = runtime.config.webapp.basePath.replace(/\/+$/u, "") || "/webapp";
|
|
123
|
+
const rootPrefix = normalizeBasePath(process.env.ROOT_PREFIX || "/api");
|
|
124
|
+
const publicWebAppBasePath = rootPrefix === "/"
|
|
125
|
+
? webAppBasePath
|
|
126
|
+
: `${rootPrefix}${normalizeBasePath(webAppBasePath)}`;
|
|
127
|
+
const webAppLivePrefix = `${webAppBasePath}/live/`;
|
|
128
|
+
const closeSessionEntry = async (entry) => {
|
|
129
|
+
await entry.close();
|
|
130
|
+
};
|
|
131
|
+
const handleRequest = async (req, res, pathname) => {
|
|
132
|
+
const method = req.method ?? "GET";
|
|
133
|
+
const normalizedPathname = normalizePrefixedPathname(pathname);
|
|
134
|
+
const requestUrl = new URL(req.url ?? normalizedPathname, `http://gateway.local`);
|
|
135
|
+
requestUrl.pathname = normalizedPathname;
|
|
136
|
+
if (requestUrl.pathname === "/healthz") {
|
|
137
|
+
writeJson(res, 200, {
|
|
138
|
+
ok: true,
|
|
139
|
+
service: "telegram-human-mcp",
|
|
140
|
+
transport: "streamable-http",
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (await runtime.gatewayHttpService.handleRequest(req, res, requestUrl.pathname)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (runtime.config.webapp.enabled) {
|
|
148
|
+
if (requestUrl.pathname === webAppBasePath ||
|
|
149
|
+
requestUrl.pathname === `${webAppBasePath}/` ||
|
|
150
|
+
requestUrl.pathname.startsWith(webAppLivePrefix)) {
|
|
151
|
+
if (method !== "GET") {
|
|
152
|
+
writeText(res, 405, "Method not allowed");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
writeHtml(res, 200, (0, assets_1.renderWebAppHtml)({
|
|
156
|
+
basePath: publicWebAppBasePath,
|
|
157
|
+
launchMode: runtime.config.webapp.launchMode,
|
|
158
|
+
}));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (requestUrl.pathname === `${webAppBasePath}/app.js`) {
|
|
162
|
+
if (method !== "GET") {
|
|
163
|
+
writeText(res, 405, "Method not allowed");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
writeJavaScript(res, 200, assets_1.WEBAPP_APP_JS);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (requestUrl.pathname === `${webAppBasePath}/styles.css`) {
|
|
170
|
+
if (method !== "GET") {
|
|
171
|
+
writeText(res, 405, "Method not allowed");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
writeCss(res, 200, assets_1.WEBAPP_STYLES_CSS);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (requestUrl.pathname === `${webAppBasePath}/api/bootstrap`) {
|
|
178
|
+
if (method !== "POST") {
|
|
179
|
+
writeText(res, 405, "Method not allowed");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
runtime.logger.info("Telegram WebApp bootstrap request received", {
|
|
183
|
+
method,
|
|
184
|
+
path: requestUrl.pathname,
|
|
185
|
+
});
|
|
186
|
+
const body = await readJsonBody(req).catch(() => undefined);
|
|
187
|
+
let sessionId = body &&
|
|
188
|
+
typeof body === "object" &&
|
|
189
|
+
typeof Reflect.get(body, "sessionId") === "string"
|
|
190
|
+
? String(Reflect.get(body, "sessionId")).trim()
|
|
191
|
+
: "";
|
|
192
|
+
const initDataRaw = body &&
|
|
193
|
+
typeof body === "object" &&
|
|
194
|
+
typeof Reflect.get(body, "initDataRaw") === "string"
|
|
195
|
+
? String(Reflect.get(body, "initDataRaw"))
|
|
196
|
+
: "";
|
|
197
|
+
const initDataUnsafe = body &&
|
|
198
|
+
typeof body === "object" &&
|
|
199
|
+
Reflect.get(body, "initDataUnsafe") &&
|
|
200
|
+
typeof Reflect.get(body, "initDataUnsafe") === "object"
|
|
201
|
+
? Reflect.get(body, "initDataUnsafe")
|
|
202
|
+
: null;
|
|
203
|
+
if (!initDataRaw || !initDataUnsafe) {
|
|
204
|
+
writeText(res, 400, "initDataRaw and initDataUnsafe are required");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const relayTarget = (0, relay_1.parseLiveRelaySessionId)(sessionId);
|
|
208
|
+
if (relayTarget) {
|
|
209
|
+
try {
|
|
210
|
+
let trustedTelegramUserId = null;
|
|
211
|
+
if (relayTarget.sourceClientUuid &&
|
|
212
|
+
relayTarget.sourceClientUuid !== relayTarget.clientUuid) {
|
|
213
|
+
const validation = await runtime.gatewayHttpService.requestLiveRelayBootstrapValidation({
|
|
214
|
+
clientUuid: relayTarget.sourceClientUuid,
|
|
215
|
+
initDataRaw,
|
|
216
|
+
initDataUnsafe,
|
|
217
|
+
});
|
|
218
|
+
trustedTelegramUserId = validation.telegram_user_id;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
try {
|
|
222
|
+
const validated = (0, auth_1.validateTelegramWebAppInitData)(initDataRaw, initDataUnsafe, runtime.config.telegram.botToken, runtime.config.webapp.initDataTtlSeconds);
|
|
223
|
+
trustedTelegramUserId = validated.user.id;
|
|
224
|
+
runtime.webAppLaunchRegistry.deleteByUserId(validated.user.id);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
trustedTelegramUserId = null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const relayBootstrap = await runtime.gatewayHttpService.requestLiveRelayBootstrap({
|
|
231
|
+
clientUuid: relayTarget.clientUuid,
|
|
232
|
+
localSessionId: relayTarget.localSessionId,
|
|
233
|
+
...(trustedTelegramUserId !== null
|
|
234
|
+
? { telegramUserId: trustedTelegramUserId }
|
|
235
|
+
: {}),
|
|
236
|
+
...(relayTarget.sourceClientUuid &&
|
|
237
|
+
relayTarget.sourceClientUuid !== relayTarget.clientUuid
|
|
238
|
+
? { allowForeignBinding: true }
|
|
239
|
+
: {}),
|
|
240
|
+
initDataRaw,
|
|
241
|
+
initDataUnsafe,
|
|
242
|
+
});
|
|
243
|
+
const record = webAppSessions.create(sessionId, relayBootstrap.telegram_user_id, runtime.config.webapp.sessionTtlSeconds);
|
|
244
|
+
writeJson(res, 200, {
|
|
245
|
+
token: record.token,
|
|
246
|
+
session_id: relayBootstrap.session_id,
|
|
247
|
+
session_label: relayBootstrap.session_label,
|
|
248
|
+
tmux_target: relayBootstrap.tmux_target,
|
|
249
|
+
poll_interval_ms: relayBootstrap.poll_interval_ms,
|
|
250
|
+
expires_at: new Date(record.expiresAtMs).toISOString(),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
runtime.logger.warn("Telegram WebApp relay bootstrap rejected", {
|
|
255
|
+
sessionId,
|
|
256
|
+
clientUuid: relayTarget.clientUuid,
|
|
257
|
+
localSessionId: relayTarget.localSessionId,
|
|
258
|
+
error: error instanceof Error
|
|
259
|
+
? (error.stack ?? error.message)
|
|
260
|
+
: String(error),
|
|
261
|
+
});
|
|
262
|
+
writeText(res, 403, error instanceof Error ? error.message : "WebApp relay bootstrap failed");
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const validated = (0, auth_1.validateTelegramWebAppInitData)(initDataRaw, initDataUnsafe, runtime.config.telegram.botToken, runtime.config.webapp.initDataTtlSeconds);
|
|
268
|
+
runtime.logger.info("Telegram WebApp initData validation debug", {
|
|
269
|
+
sessionId: sessionId || null,
|
|
270
|
+
telegramUserId: validated.user.id,
|
|
271
|
+
providedHash: validated.validationDebug.providedHash,
|
|
272
|
+
officialRawMatches: validated.validationDebug.officialRaw.matches,
|
|
273
|
+
officialRawCheckString: validated.validationDebug.officialRaw.checkString,
|
|
274
|
+
officialRawComputedHash: validated.validationDebug.officialRaw.computedHash,
|
|
275
|
+
userFieldsMatches: validated.validationDebug.userFields?.matches ?? null,
|
|
276
|
+
userFieldsCheckString: validated.validationDebug.userFields?.checkString ?? null,
|
|
277
|
+
userFieldsComputedHash: validated.validationDebug.userFields?.computedHash ?? null,
|
|
278
|
+
});
|
|
279
|
+
if (!sessionId) {
|
|
280
|
+
sessionId =
|
|
281
|
+
(await runtime.bindingStore.getActiveSessionIdForTelegramUser(validated.user.id)) ?? "";
|
|
282
|
+
}
|
|
283
|
+
let launchRecord = null;
|
|
284
|
+
if (!sessionId) {
|
|
285
|
+
const launch = runtime.webAppLaunchRegistry.getByUserId(validated.user.id);
|
|
286
|
+
launchRecord = launch;
|
|
287
|
+
sessionId = launch?.sessionId ?? "";
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
launchRecord = runtime.webAppLaunchRegistry.getByUserId(validated.user.id);
|
|
291
|
+
}
|
|
292
|
+
if (!sessionId) {
|
|
293
|
+
writeText(res, 400, "sessionId is missing and no pending Telegram WebApp launch was found");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const binding = await runtime.bindingStore.getBinding(sessionId);
|
|
297
|
+
if (!binding || binding.telegramUserId !== validated.user.id) {
|
|
298
|
+
writeText(res, 403, "This Telegram user is not bound to the requested session.");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const session = await runtime.sessionStore.getSession(sessionId);
|
|
302
|
+
const record = webAppSessions.create(sessionId, validated.user.id, runtime.config.webapp.sessionTtlSeconds);
|
|
303
|
+
runtime.logger.info("Telegram WebApp session bootstrapped", {
|
|
304
|
+
sessionId,
|
|
305
|
+
telegramUserId: validated.user.id,
|
|
306
|
+
hasTmuxTarget: Boolean(session?.tmuxTarget),
|
|
307
|
+
});
|
|
308
|
+
runtime.webAppLaunchRegistry.deleteByUserId(validated.user.id);
|
|
309
|
+
writeJson(res, 200, {
|
|
310
|
+
token: record.token,
|
|
311
|
+
session_id: sessionId,
|
|
312
|
+
session_label: session?.label ?? null,
|
|
313
|
+
tmux_target: Boolean(session?.tmuxTarget),
|
|
314
|
+
poll_interval_ms: runtime.config.webapp.pollIntervalMs,
|
|
315
|
+
expires_at: new Date(record.expiresAtMs).toISOString(),
|
|
316
|
+
});
|
|
317
|
+
if (launchRecord?.telegramChatId !== undefined &&
|
|
318
|
+
launchRecord?.telegramMessageId !== undefined) {
|
|
319
|
+
void runtime.telegramTransport
|
|
320
|
+
.deleteMessage(launchRecord.telegramChatId, launchRecord.telegramMessageId)
|
|
321
|
+
.catch((error) => {
|
|
322
|
+
runtime.logger.warn("Telegram WebApp launcher message deletion failed", {
|
|
323
|
+
sessionId,
|
|
324
|
+
telegramUserId: validated.user.id,
|
|
325
|
+
telegramChatId: launchRecord.telegramChatId,
|
|
326
|
+
telegramMessageId: launchRecord.telegramMessageId,
|
|
327
|
+
error: error instanceof Error
|
|
328
|
+
? (error.stack ?? error.message)
|
|
329
|
+
: String(error),
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
runtime.logger.warn("Telegram WebApp bootstrap rejected", {
|
|
337
|
+
sessionId,
|
|
338
|
+
initDataLength: initDataRaw.length,
|
|
339
|
+
initDataPreview: initDataRaw.slice(0, 160),
|
|
340
|
+
hasUnsafeUser: Boolean(initDataUnsafe?.user) &&
|
|
341
|
+
typeof initDataUnsafe?.user?.id === "number",
|
|
342
|
+
error: error instanceof Error
|
|
343
|
+
? (error.stack ?? error.message)
|
|
344
|
+
: String(error),
|
|
345
|
+
});
|
|
346
|
+
writeText(res, 403, error instanceof Error ? error.message : "WebApp bootstrap failed");
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (requestUrl.pathname === `${webAppBasePath}/api/view`) {
|
|
351
|
+
if (method !== "GET") {
|
|
352
|
+
writeText(res, 405, "Method not allowed");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const token = readBearerToken(req);
|
|
356
|
+
const webAppSession = token ? webAppSessions.get(token) : null;
|
|
357
|
+
if (!webAppSession) {
|
|
358
|
+
writeText(res, 401, "Unauthorized");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const relayTarget = (0, relay_1.parseLiveRelaySessionId)(webAppSession.sessionId);
|
|
362
|
+
if (relayTarget) {
|
|
363
|
+
try {
|
|
364
|
+
const result = await runtime.gatewayHttpService.requestLiveRelayView({
|
|
365
|
+
clientUuid: relayTarget.clientUuid,
|
|
366
|
+
localSessionId: relayTarget.localSessionId,
|
|
367
|
+
});
|
|
368
|
+
writeJson(res, 200, result);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
runtime.logger.error("Telegram WebApp relay visible buffer capture failed", {
|
|
372
|
+
sessionId: webAppSession.sessionId,
|
|
373
|
+
clientUuid: relayTarget.clientUuid,
|
|
374
|
+
localSessionId: relayTarget.localSessionId,
|
|
375
|
+
error: error instanceof Error
|
|
376
|
+
? (error.stack ?? error.message)
|
|
377
|
+
: String(error),
|
|
378
|
+
});
|
|
379
|
+
writeText(res, isRelayTmuxUnavailableMessage(error) ? 503 : 500, error instanceof Error ? error.message : "Failed to capture relay tmux pane");
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const binding = await runtime.bindingStore.getBinding(webAppSession.sessionId);
|
|
384
|
+
if (!binding ||
|
|
385
|
+
binding.telegramUserId !== webAppSession.telegramUserId) {
|
|
386
|
+
writeText(res, 403, "Session binding is no longer valid");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const session = await runtime.sessionStore.getSession(webAppSession.sessionId);
|
|
390
|
+
if (!session?.tmuxTarget) {
|
|
391
|
+
writeText(res, 409, "tmux target is not configured for this session");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const content = await (0, tmux_1.captureVisibleTmuxPane)(runtime.config.tmux, session.tmuxTarget, runtime.config.tmux.captureLines, runtime.config.webapp.visibleScreens);
|
|
396
|
+
writeJson(res, 200, {
|
|
397
|
+
session_id: session.sessionId,
|
|
398
|
+
session_label: session.label ?? null,
|
|
399
|
+
captured_at: new Date().toISOString(),
|
|
400
|
+
content,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
runtime.logger.error("Telegram WebApp visible buffer capture failed", {
|
|
405
|
+
sessionId: webAppSession.sessionId,
|
|
406
|
+
error: error instanceof Error
|
|
407
|
+
? (error.stack ?? error.message)
|
|
408
|
+
: String(error),
|
|
409
|
+
});
|
|
410
|
+
writeText(res, (0, tmux_1.isTmuxUnavailableError)(error) ? 503 : 500, formatTmuxHttpError(error, "Failed to capture visible tmux pane"));
|
|
411
|
+
}
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (requestUrl.pathname === `${webAppBasePath}/api/action`) {
|
|
415
|
+
if (method !== "POST") {
|
|
416
|
+
writeText(res, 405, "Method not allowed");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const token = readBearerToken(req);
|
|
420
|
+
const webAppSession = token ? webAppSessions.get(token) : null;
|
|
421
|
+
if (!webAppSession) {
|
|
422
|
+
writeText(res, 401, "Unauthorized");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const body = await readJsonBody(req).catch(() => undefined);
|
|
426
|
+
const action = body &&
|
|
427
|
+
typeof body === "object" &&
|
|
428
|
+
typeof Reflect.get(body, "action") === "string"
|
|
429
|
+
? String(Reflect.get(body, "action"))
|
|
430
|
+
: "";
|
|
431
|
+
if (!["up", "down", "enter", "slash", "delete", "tab", "escape"].includes(action)) {
|
|
432
|
+
writeText(res, 400, "Unsupported action");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const nowMs = Date.now();
|
|
436
|
+
if (nowMs - webAppSession.lastActionAtMs <
|
|
437
|
+
runtime.config.webapp.actionCooldownMs) {
|
|
438
|
+
writeText(res, 429, "Action cooldown");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const relayTarget = (0, relay_1.parseLiveRelaySessionId)(webAppSession.sessionId);
|
|
442
|
+
if (relayTarget) {
|
|
443
|
+
try {
|
|
444
|
+
await runtime.gatewayHttpService.requestLiveRelayAction({
|
|
445
|
+
clientUuid: relayTarget.clientUuid,
|
|
446
|
+
localSessionId: relayTarget.localSessionId,
|
|
447
|
+
action: action,
|
|
448
|
+
});
|
|
449
|
+
webAppSessions.touchAction(webAppSession.token, nowMs);
|
|
450
|
+
writeJson(res, 200, {
|
|
451
|
+
ok: true,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
runtime.logger.error("Telegram WebApp relay action failed", {
|
|
456
|
+
sessionId: webAppSession.sessionId,
|
|
457
|
+
clientUuid: relayTarget.clientUuid,
|
|
458
|
+
localSessionId: relayTarget.localSessionId,
|
|
459
|
+
action,
|
|
460
|
+
error: error instanceof Error
|
|
461
|
+
? (error.stack ?? error.message)
|
|
462
|
+
: String(error),
|
|
463
|
+
});
|
|
464
|
+
writeText(res, isRelayTmuxUnavailableMessage(error) ? 503 : 500, error instanceof Error ? error.message : "Failed to send relay tmux action");
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const binding = await runtime.bindingStore.getBinding(webAppSession.sessionId);
|
|
469
|
+
if (!binding ||
|
|
470
|
+
binding.telegramUserId !== webAppSession.telegramUserId) {
|
|
471
|
+
writeText(res, 403, "Session binding is no longer valid");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const session = await runtime.sessionStore.getSession(webAppSession.sessionId);
|
|
475
|
+
if (!session?.tmuxTarget) {
|
|
476
|
+
writeText(res, 409, "tmux target is not configured for this session");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
await (0, tmux_1.sendAllowedTmuxAction)(runtime.config.tmux, session.tmuxTarget, action);
|
|
481
|
+
webAppSessions.touchAction(webAppSession.token, nowMs);
|
|
482
|
+
runtime.logger.info("Telegram WebApp action sent to tmux", {
|
|
483
|
+
sessionId: webAppSession.sessionId,
|
|
484
|
+
telegramUserId: webAppSession.telegramUserId,
|
|
485
|
+
action,
|
|
486
|
+
});
|
|
487
|
+
writeJson(res, 200, {
|
|
488
|
+
ok: true,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
runtime.logger.error("Telegram WebApp action failed", {
|
|
493
|
+
sessionId: webAppSession.sessionId,
|
|
494
|
+
telegramUserId: webAppSession.telegramUserId,
|
|
495
|
+
action,
|
|
496
|
+
error: error instanceof Error
|
|
497
|
+
? (error.stack ?? error.message)
|
|
498
|
+
: String(error),
|
|
499
|
+
});
|
|
500
|
+
writeText(res, (0, tmux_1.isTmuxUnavailableError)(error) ? 503 : 500, formatTmuxHttpError(error, "Failed to send tmux action"));
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (requestUrl.pathname === "/sessions") {
|
|
506
|
+
if (!runtime.config.mcp.enableDebugRoutes) {
|
|
507
|
+
writeText(res, 404, "Not found");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (!isAuthorized(req, runtime.config.mcp.bearerToken)) {
|
|
511
|
+
runtime.logger.warn("Unauthorized sessions HTTP request rejected", {
|
|
512
|
+
method,
|
|
513
|
+
path: requestUrl.pathname,
|
|
514
|
+
remoteAddress: req.socket.remoteAddress,
|
|
515
|
+
});
|
|
516
|
+
writeText(res, 401, "Unauthorized");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (method !== "GET") {
|
|
520
|
+
writeText(res, 405, "Method not allowed");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const sessions = await runtime.sessionStore.listSessions();
|
|
524
|
+
const payload = await Promise.all(sessions
|
|
525
|
+
.sort((left, right) => left.sessionId.localeCompare(right.sessionId))
|
|
526
|
+
.map(async (session) => {
|
|
527
|
+
const binding = await runtime.bindingStore.getBinding(session.sessionId);
|
|
528
|
+
const inboxCount = await runtime.inboxStore.countInboxMessages(session.sessionId);
|
|
529
|
+
return {
|
|
530
|
+
session_id: session.sessionId,
|
|
531
|
+
session_label: session.label ?? null,
|
|
532
|
+
updated_at: session.updatedAt,
|
|
533
|
+
inbox_count: inboxCount,
|
|
534
|
+
binding: binding
|
|
535
|
+
? {
|
|
536
|
+
telegram_chat_id: binding.telegramChatId,
|
|
537
|
+
telegram_user_id: binding.telegramUserId,
|
|
538
|
+
linked_at: binding.linkedAt,
|
|
539
|
+
}
|
|
540
|
+
: null,
|
|
541
|
+
tmux: {
|
|
542
|
+
tmux_session_name: session.tmuxSessionName ?? null,
|
|
543
|
+
tmux_window_name: session.tmuxWindowName ?? null,
|
|
544
|
+
tmux_window_index: typeof session.tmuxWindowIndex === "number"
|
|
545
|
+
? session.tmuxWindowIndex
|
|
546
|
+
: null,
|
|
547
|
+
tmux_pane_id: session.tmuxPaneId ?? null,
|
|
548
|
+
tmux_pane_index: typeof session.tmuxPaneIndex === "number"
|
|
549
|
+
? session.tmuxPaneIndex
|
|
550
|
+
: null,
|
|
551
|
+
tmux_target: session.tmuxTarget ?? null,
|
|
552
|
+
last_nudge_at: session.lastTmuxNudgeAt ?? null,
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
}));
|
|
556
|
+
writeJson(res, 200, {
|
|
557
|
+
total: payload.length,
|
|
558
|
+
sessions: payload,
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (requestUrl.pathname === "/prune") {
|
|
563
|
+
if (!runtime.config.mcp.enablePruneRoute) {
|
|
564
|
+
writeText(res, 404, "Not found");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (!isAuthorized(req, runtime.config.mcp.bearerToken)) {
|
|
568
|
+
runtime.logger.warn("Unauthorized prune HTTP request rejected", {
|
|
569
|
+
method,
|
|
570
|
+
path: requestUrl.pathname,
|
|
571
|
+
remoteAddress: req.socket.remoteAddress,
|
|
572
|
+
});
|
|
573
|
+
writeText(res, 401, "Unauthorized");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (method !== "POST") {
|
|
577
|
+
writeText(res, 405, "Method not allowed");
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const result = await runtime.maintenanceStore.pruneAll();
|
|
581
|
+
runtime.logger.warn("MCP service state pruned through HTTP endpoint", {
|
|
582
|
+
deletedKeys: result.deletedKeys,
|
|
583
|
+
remoteAddress: req.socket.remoteAddress,
|
|
584
|
+
});
|
|
585
|
+
writeJson(res, 200, {
|
|
586
|
+
ok: true,
|
|
587
|
+
deleted_keys: result.deletedKeys,
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (requestUrl.pathname !== runtime.config.mcp.httpPath) {
|
|
592
|
+
writeText(res, 404, "Not found");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (!isAuthorized(req, runtime.config.mcp.bearerToken)) {
|
|
596
|
+
runtime.logger.warn("Unauthorized MCP HTTP request rejected", {
|
|
597
|
+
method,
|
|
598
|
+
path: requestUrl.pathname,
|
|
599
|
+
remoteAddress: req.socket.remoteAddress,
|
|
600
|
+
});
|
|
601
|
+
writeText(res, 401, "Unauthorized");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const sessionId = readHeader(req, "mcp-session-id");
|
|
605
|
+
const parsedBody = method === "POST" || method === "DELETE"
|
|
606
|
+
? await readJsonBody(req)
|
|
607
|
+
: undefined;
|
|
608
|
+
runtime.logger.debug("MCP HTTP request received", {
|
|
609
|
+
method,
|
|
610
|
+
path: requestUrl.pathname,
|
|
611
|
+
sessionId,
|
|
612
|
+
hasBody: parsedBody !== undefined,
|
|
613
|
+
});
|
|
614
|
+
try {
|
|
615
|
+
if (method === "POST") {
|
|
616
|
+
if (sessionId) {
|
|
617
|
+
const entry = transports.get(sessionId);
|
|
618
|
+
if (!entry) {
|
|
619
|
+
writeJson(res, 404, {
|
|
620
|
+
jsonrpc: "2.0",
|
|
621
|
+
error: {
|
|
622
|
+
code: -32001,
|
|
623
|
+
message: "Unknown MCP session",
|
|
624
|
+
},
|
|
625
|
+
id: null,
|
|
626
|
+
});
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
await entry.transport.handleRequest(req, res, parsedBody);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (!isInitializeRequest(parsedBody)) {
|
|
633
|
+
writeJson(res, 400, {
|
|
634
|
+
jsonrpc: "2.0",
|
|
635
|
+
error: {
|
|
636
|
+
code: -32000,
|
|
637
|
+
message: "Initialization request is required for a new session",
|
|
638
|
+
},
|
|
639
|
+
id: null,
|
|
640
|
+
});
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const entryRef = { current: null };
|
|
644
|
+
let knownSessionId;
|
|
645
|
+
let closing = false;
|
|
646
|
+
let closePromise = null;
|
|
647
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
648
|
+
sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
|
|
649
|
+
onsessioninitialized: (createdSessionId) => {
|
|
650
|
+
knownSessionId = createdSessionId;
|
|
651
|
+
if (entryRef.current) {
|
|
652
|
+
transports.set(createdSessionId, entryRef.current);
|
|
653
|
+
runtime.logger.info("MCP HTTP session initialized", {
|
|
654
|
+
sessionId: createdSessionId,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
const server = input.createMcpServer();
|
|
660
|
+
const closeEntry = async (initiator) => {
|
|
661
|
+
if (closing) {
|
|
662
|
+
return closePromise ?? Promise.resolve();
|
|
663
|
+
}
|
|
664
|
+
closing = true;
|
|
665
|
+
if (closePromise) {
|
|
666
|
+
return closePromise;
|
|
667
|
+
}
|
|
668
|
+
closePromise = (async () => {
|
|
669
|
+
transport.onclose = () => { };
|
|
670
|
+
if (knownSessionId) {
|
|
671
|
+
transports.delete(knownSessionId);
|
|
672
|
+
runtime.logger.info("MCP HTTP session closed", {
|
|
673
|
+
sessionId: knownSessionId,
|
|
674
|
+
initiator,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
if (initiator === "app") {
|
|
678
|
+
await transport.close();
|
|
679
|
+
}
|
|
680
|
+
await server.close();
|
|
681
|
+
})();
|
|
682
|
+
return closePromise;
|
|
683
|
+
};
|
|
684
|
+
entryRef.current = {
|
|
685
|
+
server,
|
|
686
|
+
transport,
|
|
687
|
+
close: () => closeEntry("app"),
|
|
688
|
+
};
|
|
689
|
+
transport.onclose = () => {
|
|
690
|
+
void closeEntry("transport");
|
|
691
|
+
};
|
|
692
|
+
transport.onerror = (error) => {
|
|
693
|
+
if (isDuplicateSseStreamError(error)) {
|
|
694
|
+
runtime.logger.warn("Duplicate MCP SSE stream reported by transport", {
|
|
695
|
+
sessionId: knownSessionId,
|
|
696
|
+
});
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
runtime.logger.error("MCP HTTP transport error", {
|
|
700
|
+
sessionId: knownSessionId,
|
|
701
|
+
error: error.stack ?? error.message,
|
|
702
|
+
});
|
|
703
|
+
};
|
|
704
|
+
await server.connect(transport);
|
|
705
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (method === "GET" || method === "DELETE") {
|
|
709
|
+
if (!sessionId) {
|
|
710
|
+
writeText(res, 400, "Missing MCP session ID");
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const entry = transports.get(sessionId);
|
|
714
|
+
if (!entry) {
|
|
715
|
+
writeText(res, 404, "Unknown MCP session");
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
await entry.transport.handleRequest(req, res, parsedBody);
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
if (method === "GET" && isDuplicateSseStreamError(error)) {
|
|
723
|
+
runtime.logger.warn("Duplicate MCP SSE stream detected, closing stale session", {
|
|
724
|
+
sessionId,
|
|
725
|
+
});
|
|
726
|
+
await closeSessionEntry(entry);
|
|
727
|
+
transports.delete(sessionId);
|
|
728
|
+
if (!res.headersSent) {
|
|
729
|
+
writeText(res, 409, "Duplicate SSE stream for MCP session. Reconnect and initialize a fresh session.");
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
writeText(res, 405, "Method not allowed");
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
runtime.logger.error("Error handling MCP HTTP request", {
|
|
741
|
+
method,
|
|
742
|
+
path: requestUrl.pathname,
|
|
743
|
+
sessionId,
|
|
744
|
+
error: error instanceof Error
|
|
745
|
+
? (error.stack ?? error.message)
|
|
746
|
+
: String(error),
|
|
747
|
+
});
|
|
748
|
+
if (!res.headersSent) {
|
|
749
|
+
writeJson(res, 500, {
|
|
750
|
+
jsonrpc: "2.0",
|
|
751
|
+
error: {
|
|
752
|
+
code: -32603,
|
|
753
|
+
message: "Internal server error",
|
|
754
|
+
},
|
|
755
|
+
id: null,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
let shuttingDown = false;
|
|
761
|
+
const shutdown = async () => {
|
|
762
|
+
if (shuttingDown) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
shuttingDown = true;
|
|
766
|
+
runtime.logger.info("MCP HTTP handler shutdown requested");
|
|
767
|
+
await Promise.all(Array.from(transports.values()).map((entry) => closeSessionEntry(entry)));
|
|
768
|
+
transports.clear();
|
|
769
|
+
};
|
|
770
|
+
return {
|
|
771
|
+
handleRequest,
|
|
772
|
+
close: shutdown,
|
|
773
|
+
};
|
|
774
|
+
}
|