@deadragdoll/tellymcp 0.0.12 → 0.0.14
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 +5 -1
- package/.env.example.gateway +16 -1
- package/CHANGELOG.md +30 -0
- package/README-ru.md +33 -0
- package/README.md +33 -0
- package/TOOLS.md +384 -9
- package/config/templates/env.both.template +15 -2
- package/config/templates/env.client.template +5 -2
- package/config/templates/env.gateway.template +15 -2
- package/dist/cli.js +137 -2
- package/dist/services/features/telegram-mcp/browser.service.js +56 -1
- package/dist/services/features/telegram-mcp/file-content.service.js +94 -0
- package/dist/services/features/telegram-mcp/gateway-delivery.service.js +5 -1
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +43 -11
- package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -0
- package/dist/services/features/telegram-mcp/mcp-server.service.js +30 -0
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +23 -1
- package/dist/services/features/telegram-mcp/src/app/config/env.js +132 -2
- package/dist/services/features/telegram-mcp/src/app/http.js +139 -99
- package/dist/services/features/telegram-mcp/src/app/oauthFacade.js +642 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +151 -170
- package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +96 -99
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +205 -17
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachActiveTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDetachTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserInjectScriptTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStartTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStatusTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStopTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +890 -23
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +536 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +80 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +736 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +188 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +2 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +6 -3
- package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +4 -1
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +33 -35
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileListTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileService.js +327 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileTool.js +81 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/temporaryFileLinkStore.js +307 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/workspaceFilePolicy.js +115 -0
- package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +5 -1
- package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +28 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +3 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportFileHandoffActions.js +6 -3
- package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/client.js +29 -6
- package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/ptyRegistry.js +100 -2
- package/dist/services/features/telegram-mcp/src/shared/lib/bodyLimits.js +63 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/gatewayAuth.js +13 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/time/localTimestamp.js +21 -0
- package/docs/CHAT_CONNECTOR.md +134 -0
- package/docs/STANDALONE-ru.md +55 -6
- package/docs/STANDALONE.md +55 -6
- package/package.json +10 -5
- package/packages/chrome-attach-extension/dist/background.js +1735 -0
- package/packages/chrome-attach-extension/dist/icon.svg +6 -0
- package/packages/chrome-attach-extension/dist/manifest.json +37 -0
- package/packages/chrome-attach-extension/dist/options.html +312 -0
- package/packages/chrome-attach-extension/dist/options.js +606 -0
- package/packages/chrome-attach-extension/dist/popup.html +93 -0
- package/packages/chrome-attach-extension/dist/popup.js +79 -0
- package/packages/chrome-attach-extension/dist/recorder-content.js +96 -0
- package/packages/chrome-attach-extension/dist/recorder-page.js +282 -0
- package/packages/firefox-attach-extension/README.md +13 -0
- package/packages/firefox-attach-extension/dist/background.js +1622 -0
- package/packages/firefox-attach-extension/dist/icon.svg +6 -0
- package/packages/firefox-attach-extension/dist/manifest.json +44 -0
- package/packages/firefox-attach-extension/dist/options.html +312 -0
- package/packages/firefox-attach-extension/dist/options.js +540 -0
- package/packages/firefox-attach-extension/dist/popup.html +93 -0
- package/packages/firefox-attach-extension/dist/popup.js +64 -0
- package/packages/firefox-attach-extension/dist/recorder-content.js +90 -0
- package/packages/firefox-attach-extension/dist/recorder-page.js +318 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GetFileService = void 0;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_stream_1 = require("node:stream");
|
|
11
|
+
const client_1 = require("../../../shared/integrations/terminal/client");
|
|
12
|
+
const bodyLimits_1 = require("../../../shared/lib/bodyLimits");
|
|
13
|
+
const temporaryFileLinkStore_1 = require("./temporaryFileLinkStore");
|
|
14
|
+
const workspaceFilePolicy_1 = require("./workspaceFilePolicy");
|
|
15
|
+
const MAX_NATIVE_IMAGE_SOURCE_SIZE_BYTES = 11 * 1024 * 1024;
|
|
16
|
+
const MAX_TEXT_SOURCE_SIZE_BYTES = 7 * 1024 * 1024;
|
|
17
|
+
function normalizeWorkspaceRelativePath(workspaceDir, filePath) {
|
|
18
|
+
const trimmed = filePath.trim();
|
|
19
|
+
const resolvedWorkspaceDir = node_path_1.default.resolve(workspaceDir);
|
|
20
|
+
const resolvedFilePath = node_path_1.default.isAbsolute(trimmed)
|
|
21
|
+
? node_path_1.default.resolve(trimmed)
|
|
22
|
+
: node_path_1.default.resolve(resolvedWorkspaceDir, trimmed);
|
|
23
|
+
const relative = node_path_1.default.relative(resolvedWorkspaceDir, resolvedFilePath);
|
|
24
|
+
if (relative.startsWith("..") ||
|
|
25
|
+
node_path_1.default.isAbsolute(relative) ||
|
|
26
|
+
relative.trim() === "") {
|
|
27
|
+
throw new Error("File path is outside the workspace directory.");
|
|
28
|
+
}
|
|
29
|
+
return relative.split(node_path_1.default.sep).join("/");
|
|
30
|
+
}
|
|
31
|
+
class GetFileService {
|
|
32
|
+
config;
|
|
33
|
+
sessionStore;
|
|
34
|
+
maintenanceStore;
|
|
35
|
+
xchangeFileMetaStore;
|
|
36
|
+
logger;
|
|
37
|
+
projectIdentityResolver;
|
|
38
|
+
remoteConsoleInvoker;
|
|
39
|
+
temporaryFileLinkStore;
|
|
40
|
+
constructor(config, sessionStore, maintenanceStore, xchangeFileMetaStore, logger, projectIdentityResolver, remoteConsoleInvoker, temporaryFileLinkStore) {
|
|
41
|
+
this.config = config;
|
|
42
|
+
this.sessionStore = sessionStore;
|
|
43
|
+
this.maintenanceStore = maintenanceStore;
|
|
44
|
+
this.xchangeFileMetaStore = xchangeFileMetaStore;
|
|
45
|
+
this.logger = logger;
|
|
46
|
+
this.projectIdentityResolver = projectIdentityResolver;
|
|
47
|
+
this.remoteConsoleInvoker = remoteConsoleInvoker;
|
|
48
|
+
this.temporaryFileLinkStore = temporaryFileLinkStore;
|
|
49
|
+
}
|
|
50
|
+
async get(input) {
|
|
51
|
+
if (input.file_path?.trim()) {
|
|
52
|
+
(0, workspaceFilePolicy_1.assertWorkspaceFilePathAllowed)(input.file_path.trim());
|
|
53
|
+
}
|
|
54
|
+
const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
|
|
55
|
+
const sessionId = await this.normalizeSessionIdForWorkspace(resolved.sessionId);
|
|
56
|
+
const responseType = input.type ?? "url";
|
|
57
|
+
const cacheKey = this.createCacheKey(sessionId, input);
|
|
58
|
+
if (this.config.distributed.mode !== "client") {
|
|
59
|
+
if (responseType === "url" || responseType === "image") {
|
|
60
|
+
if (!this.temporaryFileLinkStore) {
|
|
61
|
+
throw new Error("URL file delivery requires gateway mode with GATEWAY_PUBLIC_URL.");
|
|
62
|
+
}
|
|
63
|
+
const ticket = this.temporaryFileLinkStore.createTicket(cacheKey);
|
|
64
|
+
try {
|
|
65
|
+
if (!this.remoteConsoleInvoker) {
|
|
66
|
+
throw new Error("Gateway remote console routing is unavailable.");
|
|
67
|
+
}
|
|
68
|
+
await this.remoteConsoleInvoker.invokeForRelaySession(sessionId, "telegramMcp.fileContent.uploadFileRemote", {
|
|
69
|
+
...input,
|
|
70
|
+
type: "url",
|
|
71
|
+
session_id: sessionId,
|
|
72
|
+
upload_url: ticket.upload_url,
|
|
73
|
+
});
|
|
74
|
+
const link = this.temporaryFileLinkStore.getReadyLink(ticket.download_token);
|
|
75
|
+
const output = {
|
|
76
|
+
type: "url",
|
|
77
|
+
data: link.url,
|
|
78
|
+
filename: link.filename,
|
|
79
|
+
mimetype: link.mimetype,
|
|
80
|
+
size_bytes: link.size_bytes,
|
|
81
|
+
expires_at: link.expires_at,
|
|
82
|
+
};
|
|
83
|
+
if (responseType === "image") {
|
|
84
|
+
if (!link.mimetype.toLowerCase().startsWith("image/")) {
|
|
85
|
+
throw new Error(`type='image' requires an image MIME type, received '${link.mimetype}'.`);
|
|
86
|
+
}
|
|
87
|
+
const nativeImage = await this.temporaryFileLinkStore.readCachedBase64(cacheKey, MAX_NATIVE_IMAGE_SOURCE_SIZE_BYTES);
|
|
88
|
+
if (!nativeImage) {
|
|
89
|
+
throw new Error("Image is too large for native MCP image content; retry with type='url'.");
|
|
90
|
+
}
|
|
91
|
+
if (!nativeImage.data || nativeImage.data === "[image]") {
|
|
92
|
+
throw new Error("Native MCP image base64 payload is empty.");
|
|
93
|
+
}
|
|
94
|
+
const decodedImageBytes = Buffer.from(nativeImage.data, "base64").byteLength;
|
|
95
|
+
if (decodedImageBytes !== link.size_bytes) {
|
|
96
|
+
throw new Error(`Native MCP image payload size mismatch: expected ${link.size_bytes} bytes, decoded ${decodedImageBytes} bytes.`);
|
|
97
|
+
}
|
|
98
|
+
const imageOutput = {
|
|
99
|
+
...output,
|
|
100
|
+
type: "image",
|
|
101
|
+
native_image_data: nativeImage.data,
|
|
102
|
+
};
|
|
103
|
+
(0, bodyLimits_1.assertSerializedBodySize)(imageOutput);
|
|
104
|
+
this.logger.info("Native MCP image content prepared", {
|
|
105
|
+
sessionId,
|
|
106
|
+
filename: link.filename,
|
|
107
|
+
mimetype: link.mimetype,
|
|
108
|
+
sizeBytes: link.size_bytes,
|
|
109
|
+
base64Chars: nativeImage.data.length,
|
|
110
|
+
decodedBytes: decodedImageBytes,
|
|
111
|
+
});
|
|
112
|
+
return imageOutput;
|
|
113
|
+
}
|
|
114
|
+
(0, bodyLimits_1.assertSerializedBodySize)(output);
|
|
115
|
+
return output;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
await this.temporaryFileLinkStore.discard(ticket.download_token);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (responseType === "base64") {
|
|
123
|
+
const cached = await this.temporaryFileLinkStore?.readCachedBase64(cacheKey, bodyLimits_1.MAX_BASE64_SOURCE_SIZE_BYTES);
|
|
124
|
+
if (cached) {
|
|
125
|
+
const output = {
|
|
126
|
+
type: "base64",
|
|
127
|
+
data: cached.data,
|
|
128
|
+
filename: cached.filename,
|
|
129
|
+
mimetype: cached.mimetype,
|
|
130
|
+
size_bytes: cached.size_bytes,
|
|
131
|
+
};
|
|
132
|
+
(0, bodyLimits_1.assertSerializedBodySize)(output);
|
|
133
|
+
return output;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const remote = await this.remoteConsoleInvoker?.invokeForRelaySession(sessionId, "telegramMcp.fileContent.getFileRemote", {
|
|
137
|
+
...input,
|
|
138
|
+
type: responseType,
|
|
139
|
+
session_id: sessionId,
|
|
140
|
+
});
|
|
141
|
+
if (remote) {
|
|
142
|
+
(0, bodyLimits_1.assertSerializedBodySize)(remote);
|
|
143
|
+
return remote;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (responseType === "url" || responseType === "image") {
|
|
147
|
+
throw new Error("URL and native image delivery must be initiated through the gateway MCP.");
|
|
148
|
+
}
|
|
149
|
+
const session = await this.sessionStore.getSession(sessionId);
|
|
150
|
+
const workspaceDir = node_path_1.default.resolve(session?.cwd?.trim() || resolved.cwd);
|
|
151
|
+
const requestedFilePath = await this.resolveRequestedFilePath(sessionId, input);
|
|
152
|
+
const relativeFilePath = normalizeWorkspaceRelativePath(workspaceDir, requestedFilePath);
|
|
153
|
+
(0, workspaceFilePolicy_1.assertWorkspaceFilePathAllowed)(relativeFilePath);
|
|
154
|
+
const resolvedFile = await (0, client_1.resolveWorkspaceFileForRead)(this.config.terminal, workspaceDir, relativeFilePath, responseType === "text"
|
|
155
|
+
? MAX_TEXT_SOURCE_SIZE_BYTES
|
|
156
|
+
: bodyLimits_1.MAX_BASE64_SOURCE_SIZE_BYTES);
|
|
157
|
+
const resolvedRelativeFilePath = normalizeWorkspaceRelativePath(workspaceDir, resolvedFile.filePath);
|
|
158
|
+
(0, workspaceFilePolicy_1.assertWorkspaceFilePathAllowed)(resolvedRelativeFilePath);
|
|
159
|
+
const fileContent = await (0, promises_1.readFile)(resolvedFile.filePath);
|
|
160
|
+
const filename = node_path_1.default.basename(relativeFilePath);
|
|
161
|
+
const mimetype = (0, workspaceFilePolicy_1.resolveWorkspaceFileMimeType)(filename);
|
|
162
|
+
const output = {
|
|
163
|
+
type: responseType,
|
|
164
|
+
data: responseType === "text"
|
|
165
|
+
? (0, workspaceFilePolicy_1.decodeWorkspaceTextContent)(fileContent)
|
|
166
|
+
: Buffer.from(fileContent).toString("base64"),
|
|
167
|
+
mimetype,
|
|
168
|
+
filename,
|
|
169
|
+
size_bytes: fileContent.byteLength,
|
|
170
|
+
};
|
|
171
|
+
(0, bodyLimits_1.assertSerializedBodySize)(output);
|
|
172
|
+
this.logger.info("Workspace file content retrieved", {
|
|
173
|
+
sessionId,
|
|
174
|
+
filename,
|
|
175
|
+
sizeBytes: fileContent.byteLength,
|
|
176
|
+
mimetype: output.mimetype,
|
|
177
|
+
});
|
|
178
|
+
return output;
|
|
179
|
+
}
|
|
180
|
+
async upload(input) {
|
|
181
|
+
if (this.config.distributed.mode !== "client") {
|
|
182
|
+
throw new Error("Temporary file uploads can only run on a client console.");
|
|
183
|
+
}
|
|
184
|
+
this.assertAllowedUploadUrl(input.upload_url);
|
|
185
|
+
const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
|
|
186
|
+
const sessionId = await this.normalizeSessionIdForWorkspace(resolved.sessionId);
|
|
187
|
+
const session = await this.sessionStore.getSession(sessionId);
|
|
188
|
+
const workspaceDir = node_path_1.default.resolve(session?.cwd?.trim() || resolved.cwd);
|
|
189
|
+
const requestedFilePath = await this.resolveRequestedFilePath(sessionId, input);
|
|
190
|
+
const relativeFilePath = normalizeWorkspaceRelativePath(workspaceDir, requestedFilePath);
|
|
191
|
+
(0, workspaceFilePolicy_1.assertWorkspaceFilePathAllowed)(relativeFilePath);
|
|
192
|
+
const resolvedFile = await (0, client_1.resolveWorkspaceFileForRead)(this.config.terminal, workspaceDir, relativeFilePath, temporaryFileLinkStore_1.TEMPORARY_FILE_LINK_MAX_BYTES);
|
|
193
|
+
const resolvedRelativeFilePath = normalizeWorkspaceRelativePath(workspaceDir, resolvedFile.filePath);
|
|
194
|
+
(0, workspaceFilePolicy_1.assertWorkspaceFilePathAllowed)(resolvedRelativeFilePath);
|
|
195
|
+
const filename = node_path_1.default.basename(relativeFilePath);
|
|
196
|
+
const mimetype = (0, workspaceFilePolicy_1.resolveWorkspaceFileMimeType)(filename);
|
|
197
|
+
const body = node_stream_1.Readable.toWeb((0, node_fs_1.createReadStream)(resolvedFile.filePath));
|
|
198
|
+
const response = await fetch(input.upload_url, {
|
|
199
|
+
method: "PUT",
|
|
200
|
+
headers: {
|
|
201
|
+
"content-type": mimetype,
|
|
202
|
+
"content-length": String(resolvedFile.sizeBytes),
|
|
203
|
+
"x-telly-filename": encodeURIComponent(filename),
|
|
204
|
+
},
|
|
205
|
+
body,
|
|
206
|
+
duplex: "half",
|
|
207
|
+
});
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
const message = await response.text();
|
|
210
|
+
throw new Error(`Gateway temporary file upload failed with status ${response.status}: ${message || response.statusText}`);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
uploaded: true,
|
|
214
|
+
filename,
|
|
215
|
+
mimetype,
|
|
216
|
+
size_bytes: resolvedFile.sizeBytes,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async list(input) {
|
|
220
|
+
const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
|
|
221
|
+
const sessionId = await this.normalizeSessionIdForWorkspace(resolved.sessionId);
|
|
222
|
+
if (this.config.distributed.mode !== "client") {
|
|
223
|
+
const remote = await this.remoteConsoleInvoker?.invokeForRelaySession(sessionId, "telegramMcp.fileContent.listFilesRemote", {
|
|
224
|
+
...input,
|
|
225
|
+
session_id: sessionId,
|
|
226
|
+
});
|
|
227
|
+
if (remote) {
|
|
228
|
+
(0, bodyLimits_1.assertSerializedBodySize)(remote);
|
|
229
|
+
return remote;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const metas = await this.xchangeFileMetaStore.listXchangeFileMetas(sessionId);
|
|
233
|
+
const filtered = metas
|
|
234
|
+
.filter((item) => !input.source || item.source === input.source)
|
|
235
|
+
.sort((left, right) => right.uploadedAt.localeCompare(left.uploadedAt));
|
|
236
|
+
const limit = input.limit ?? 50;
|
|
237
|
+
const output = {
|
|
238
|
+
total: filtered.length,
|
|
239
|
+
files: filtered.slice(0, limit).map((item) => {
|
|
240
|
+
const filename = item.originalName ||
|
|
241
|
+
node_path_1.default.basename(item.relativePath?.trim() || item.filePath);
|
|
242
|
+
return {
|
|
243
|
+
file_path: item.filePath,
|
|
244
|
+
filename,
|
|
245
|
+
mimetype: item.mimeType || (0, workspaceFilePolicy_1.resolveWorkspaceFileMimeType)(filename),
|
|
246
|
+
source: item.source,
|
|
247
|
+
...(typeof item.sizeBytes === "number"
|
|
248
|
+
? { size_bytes: item.sizeBytes }
|
|
249
|
+
: {}),
|
|
250
|
+
created_at: item.uploadedAt,
|
|
251
|
+
};
|
|
252
|
+
}),
|
|
253
|
+
};
|
|
254
|
+
(0, bodyLimits_1.assertSerializedBodySize)(output);
|
|
255
|
+
this.logger.info("Workspace file list retrieved", {
|
|
256
|
+
sessionId,
|
|
257
|
+
source: input.source ?? "all",
|
|
258
|
+
total: output.total,
|
|
259
|
+
returned: output.files.length,
|
|
260
|
+
});
|
|
261
|
+
return output;
|
|
262
|
+
}
|
|
263
|
+
async resolveRequestedFilePath(sessionId, input) {
|
|
264
|
+
if (input.file_path?.trim()) {
|
|
265
|
+
if (input.selector) {
|
|
266
|
+
throw new Error("Provide exactly one of file_path or selector.");
|
|
267
|
+
}
|
|
268
|
+
return input.file_path.trim();
|
|
269
|
+
}
|
|
270
|
+
if (input.selector !== "latest_screenshot") {
|
|
271
|
+
throw new Error("Provide exactly one of file_path or selector.");
|
|
272
|
+
}
|
|
273
|
+
const screenshots = (await this.xchangeFileMetaStore.listXchangeFileMetas(sessionId))
|
|
274
|
+
.filter((item) => item.source === "browser-screenshot")
|
|
275
|
+
.sort((left, right) => right.uploadedAt.localeCompare(left.uploadedAt));
|
|
276
|
+
const latest = screenshots[0];
|
|
277
|
+
if (!latest) {
|
|
278
|
+
throw new Error("No browser screenshots were found for this session.");
|
|
279
|
+
}
|
|
280
|
+
return latest.filePath;
|
|
281
|
+
}
|
|
282
|
+
async normalizeSessionIdForWorkspace(sessionId) {
|
|
283
|
+
const trimmed = sessionId.trim();
|
|
284
|
+
if (!trimmed) {
|
|
285
|
+
return trimmed;
|
|
286
|
+
}
|
|
287
|
+
const direct = await this.sessionStore.getSession(trimmed);
|
|
288
|
+
if (direct) {
|
|
289
|
+
return trimmed;
|
|
290
|
+
}
|
|
291
|
+
const separatorIndex = trimmed.indexOf(":");
|
|
292
|
+
if (separatorIndex <= 0) {
|
|
293
|
+
return trimmed;
|
|
294
|
+
}
|
|
295
|
+
const localClientUuid = await this.maintenanceStore.getGatewayClientUuid();
|
|
296
|
+
const clientUuid = trimmed.slice(0, separatorIndex).trim();
|
|
297
|
+
const localSessionId = trimmed.slice(separatorIndex + 1).trim();
|
|
298
|
+
if (!localClientUuid || clientUuid !== localClientUuid || !localSessionId) {
|
|
299
|
+
return trimmed;
|
|
300
|
+
}
|
|
301
|
+
const localSession = await this.sessionStore.getSession(localSessionId);
|
|
302
|
+
return localSession ? localSessionId : trimmed;
|
|
303
|
+
}
|
|
304
|
+
createCacheKey(sessionId, input) {
|
|
305
|
+
return JSON.stringify({
|
|
306
|
+
session_id: sessionId,
|
|
307
|
+
...(input.file_path?.trim() ? { file_path: input.file_path.trim() } : {}),
|
|
308
|
+
...(input.selector ? { selector: input.selector } : {}),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
assertAllowedUploadUrl(uploadUrl) {
|
|
312
|
+
const expectedBaseUrl = (0, temporaryFileLinkStore_1.resolvePublicFilesBaseUrl)(this.config.distributed.gatewayPublicUrl);
|
|
313
|
+
const candidateUrl = new URL(uploadUrl);
|
|
314
|
+
const uploadPathPrefix = `${expectedBaseUrl.pathname.replace(/\/+$/u, "")}/upload/`;
|
|
315
|
+
const uploadToken = candidateUrl.pathname.slice(uploadPathPrefix.length);
|
|
316
|
+
if (candidateUrl.origin !== expectedBaseUrl.origin ||
|
|
317
|
+
candidateUrl.username ||
|
|
318
|
+
candidateUrl.password ||
|
|
319
|
+
candidateUrl.search ||
|
|
320
|
+
candidateUrl.hash ||
|
|
321
|
+
!candidateUrl.pathname.startsWith(uploadPathPrefix) ||
|
|
322
|
+
!/^[A-Za-z0-9_-]+$/u.test(uploadToken)) {
|
|
323
|
+
throw new Error("Temporary file upload URL does not belong to the configured gateway.");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
exports.GetFileService = GetFileService;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GetFileTool = void 0;
|
|
4
|
+
const schema_1 = require("../../../entities/request/model/schema");
|
|
5
|
+
const bodyLimits_1 = require("../../../shared/lib/bodyLimits");
|
|
6
|
+
function createStructuredOutput(output) {
|
|
7
|
+
return {
|
|
8
|
+
type: output.type,
|
|
9
|
+
data: output.data,
|
|
10
|
+
mimetype: output.mimetype,
|
|
11
|
+
filename: output.filename,
|
|
12
|
+
size_bytes: output.size_bytes,
|
|
13
|
+
...(output.expires_at ? { expires_at: output.expires_at } : {}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function assertNativeImagePayload(data, sizeBytes) {
|
|
17
|
+
if (!data ||
|
|
18
|
+
data === "[image]" ||
|
|
19
|
+
Buffer.from(data, "base64").byteLength !== sizeBytes) {
|
|
20
|
+
throw new Error("Native MCP image payload failed final tool-result validation.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class GetFileTool {
|
|
24
|
+
getFileService;
|
|
25
|
+
constructor(getFileService) {
|
|
26
|
+
this.getFileService = getFileService;
|
|
27
|
+
}
|
|
28
|
+
register(server) {
|
|
29
|
+
server.registerTool("get_file", {
|
|
30
|
+
title: "Get File",
|
|
31
|
+
description: "Retrieve a file from the workspace of a selected live console. type='url' is the default and returns only a short-lived HTTPS download link. type='image' returns a native MCP image plus its URL. type='text' returns UTF-8 project files such as Markdown or source code directly as MCP text. type='base64' is the host-independent fallback and returns the complete JSON payload, including data, in an MCP text block. Provide an exact workspace-relative or absolute file_path, or selector='latest_screenshot'. Sensitive paths such as environment files, credential stores, and private keys are blocked. In gateway mode, pass session_id exactly as returned by list_gateway_sessions.",
|
|
32
|
+
inputSchema: schema_1.getFileInputSchema,
|
|
33
|
+
outputSchema: schema_1.getFileOutputSchema,
|
|
34
|
+
}, async (args) => {
|
|
35
|
+
const output = await this.getFileService.get(args);
|
|
36
|
+
if (output.native_image_data !== undefined) {
|
|
37
|
+
assertNativeImagePayload(output.native_image_data, output.size_bytes);
|
|
38
|
+
const structuredOutput = createStructuredOutput(output);
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "image",
|
|
43
|
+
data: output.native_image_data,
|
|
44
|
+
mimeType: output.mimetype,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `${output.filename}\nDownload URL: ${output.data}`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
structuredContent: structuredOutput,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (output.type === "text") {
|
|
55
|
+
const result = {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: output.data,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
structuredContent: output,
|
|
63
|
+
};
|
|
64
|
+
(0, bodyLimits_1.assertSerializedBodySize)(result);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
const result = {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: JSON.stringify(output, null, 2),
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
structuredContent: output,
|
|
75
|
+
};
|
|
76
|
+
(0, bodyLimits_1.assertSerializedBodySize)(result);
|
|
77
|
+
return result;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.GetFileTool = GetFileTool;
|