claude-threads 1.11.0 → 1.12.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/CHANGELOG.md +13 -0
- package/dist/index.js +679 -443
- package/dist/mcp/permission-server.js +374 -6
- package/package.json +1 -1
|
@@ -50934,6 +50934,26 @@ function createMessageManagerEvents() {
|
|
|
50934
50934
|
return new TypedEventEmitter;
|
|
50935
50935
|
}
|
|
50936
50936
|
|
|
50937
|
+
// src/utils/safe-filename.ts
|
|
50938
|
+
import { basename } from "path";
|
|
50939
|
+
function sanitizeFilename(name) {
|
|
50940
|
+
const flat = basename(name.replace(/\\/g, "/"));
|
|
50941
|
+
const cleaned = flat.replace(/[\x00-\x1F\x7F]/g, "_").trim();
|
|
50942
|
+
if (!cleaned || cleaned === "." || cleaned === "..") {
|
|
50943
|
+
return "attachment";
|
|
50944
|
+
}
|
|
50945
|
+
return cleaned;
|
|
50946
|
+
}
|
|
50947
|
+
function formatBytes(bytes) {
|
|
50948
|
+
if (bytes < 1024)
|
|
50949
|
+
return `${bytes} B`;
|
|
50950
|
+
if (bytes < 1024 * 1024)
|
|
50951
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
50952
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
50953
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
50954
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
50955
|
+
}
|
|
50956
|
+
|
|
50937
50957
|
// src/operations/streaming/handler.ts
|
|
50938
50958
|
var log2 = createLogger("streaming");
|
|
50939
50959
|
var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
@@ -54702,6 +54722,14 @@ import { existsSync as existsSync4, readFileSync as readFileSync3, watchFile, un
|
|
|
54702
54722
|
import { tmpdir } from "os";
|
|
54703
54723
|
import { join as join3 } from "path";
|
|
54704
54724
|
|
|
54725
|
+
// src/mcp/outbound-env.ts
|
|
54726
|
+
var OUTBOUND_ENV = {
|
|
54727
|
+
SESSION_WORKING_DIR: "SESSION_WORKING_DIR",
|
|
54728
|
+
SESSION_UPLOAD_DIR: "SESSION_UPLOAD_DIR",
|
|
54729
|
+
OUTBOUND_FILES_ENABLED: "OUTBOUND_FILES_ENABLED",
|
|
54730
|
+
OUTBOUND_FILES_MAX_BYTES: "OUTBOUND_FILES_MAX_BYTES"
|
|
54731
|
+
};
|
|
54732
|
+
|
|
54705
54733
|
// src/claude/rate-limit-detector.ts
|
|
54706
54734
|
var RATE_LIMIT_PHRASES = [
|
|
54707
54735
|
/usage limit reached/i,
|
|
@@ -54823,7 +54851,7 @@ function materializeMcpConfig(config3, sessionId, opts = {}) {
|
|
|
54823
54851
|
}
|
|
54824
54852
|
function buildPermissionArgs(opts) {
|
|
54825
54853
|
const args = [];
|
|
54826
|
-
if (opts.permissionMode === "bypass") {
|
|
54854
|
+
if (opts.permissionMode === "bypass" && !opts.platformConfig) {
|
|
54827
54855
|
args.push("--dangerously-skip-permissions");
|
|
54828
54856
|
return { args, tempFile: null };
|
|
54829
54857
|
}
|
|
@@ -54843,6 +54871,18 @@ function buildPermissionArgs(opts) {
|
|
|
54843
54871
|
if (opts.platformConfig.appToken) {
|
|
54844
54872
|
mcpEnv.PLATFORM_APP_TOKEN = opts.platformConfig.appToken;
|
|
54845
54873
|
}
|
|
54874
|
+
if (opts.workingDir) {
|
|
54875
|
+
mcpEnv[OUTBOUND_ENV.SESSION_WORKING_DIR] = opts.workingDir;
|
|
54876
|
+
}
|
|
54877
|
+
if (opts.uploadDir) {
|
|
54878
|
+
mcpEnv[OUTBOUND_ENV.SESSION_UPLOAD_DIR] = opts.uploadDir;
|
|
54879
|
+
}
|
|
54880
|
+
if (opts.outboundFiles?.enabled === false) {
|
|
54881
|
+
mcpEnv[OUTBOUND_ENV.OUTBOUND_FILES_ENABLED] = "0";
|
|
54882
|
+
}
|
|
54883
|
+
if (typeof opts.outboundFiles?.maxBytes === "number" && Number.isFinite(opts.outboundFiles.maxBytes) && opts.outboundFiles.maxBytes > 0) {
|
|
54884
|
+
mcpEnv[OUTBOUND_ENV.OUTBOUND_FILES_MAX_BYTES] = String(opts.outboundFiles.maxBytes);
|
|
54885
|
+
}
|
|
54846
54886
|
const mcpConfig = {
|
|
54847
54887
|
mcpServers: {
|
|
54848
54888
|
"claude-threads-permissions": {
|
|
@@ -54861,9 +54901,13 @@ function buildPermissionArgs(opts) {
|
|
|
54861
54901
|
} else {
|
|
54862
54902
|
args.push("--mcp-config", materialized.value);
|
|
54863
54903
|
}
|
|
54864
|
-
|
|
54865
|
-
|
|
54866
|
-
|
|
54904
|
+
if (opts.permissionMode === "bypass") {
|
|
54905
|
+
args.push("--dangerously-skip-permissions");
|
|
54906
|
+
} else {
|
|
54907
|
+
args.push("--permission-prompt-tool", "mcp__claude-threads-permissions__permission_prompt");
|
|
54908
|
+
if (opts.permissionMode === "auto") {
|
|
54909
|
+
args.push("--permission-mode", "auto");
|
|
54910
|
+
}
|
|
54867
54911
|
}
|
|
54868
54912
|
return { args, tempFile };
|
|
54869
54913
|
}
|
|
@@ -54958,7 +55002,10 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
54958
55002
|
threadId: this.options.threadId,
|
|
54959
55003
|
sessionId: this.options.sessionId,
|
|
54960
55004
|
permissionTimeoutMs: this.options.permissionTimeoutMs ?? 120000,
|
|
54961
|
-
debug: this.debug
|
|
55005
|
+
debug: this.debug,
|
|
55006
|
+
workingDir: this.options.workingDir,
|
|
55007
|
+
uploadDir: this.options.uploadDir,
|
|
55008
|
+
outboundFiles: this.options.outboundFiles
|
|
54962
55009
|
});
|
|
54963
55010
|
args.push(...permResult.args);
|
|
54964
55011
|
this.mcpConfigTempFile = permResult.tempFile;
|
|
@@ -55730,6 +55777,15 @@ You are running inside a chat platform (like Mattermost or Slack). Users interac
|
|
|
55730
55777
|
- Keep responses concise - very long responses are split across multiple messages
|
|
55731
55778
|
- Multiple users may participate in a session (the owner can invite others)
|
|
55732
55779
|
|
|
55780
|
+
## Sending files into THIS thread
|
|
55781
|
+
You are RIGHT NOW running inside a chat thread (Mattermost or Slack). The \`send_file\` MCP tool — exposed as \`mcp__claude-threads-permissions__send_file\` in your tool list — uploads a file from your working directory and posts it directly into THIS thread, where the user is talking to you. It is NOT a hypothetical capability that requires extra setup; it works for the session you are in right now.
|
|
55782
|
+
|
|
55783
|
+
Use it whenever the user asks to "send", "share", "show", or "post" a file, OR whenever you produce an artifact (screenshot, generated audio, plot, document, PDF) that the user would benefit from seeing inline rather than as a path to read.
|
|
55784
|
+
|
|
55785
|
+
Arguments: \`{ path: <absolute path inside the working directory>, caption?: <optional one-line message> }\`. Returns a JSON envelope: \`{ ok: true, postId }\` on success or \`{ ok: false, reason }\` on failure — when it fails, surface \`reason\` to the user verbatim so they understand what went wrong (e.g. "outside the working directory", "file too large").
|
|
55786
|
+
|
|
55787
|
+
Do NOT tell the user the tool isn't available, doesn't apply, or requires Mattermost — it's wired up and pointed at this very thread. Just call it.
|
|
55788
|
+
|
|
55733
55789
|
## Permissions & Interactions
|
|
55734
55790
|
- Permission requests (file writes, commands, etc.) appear as messages with emoji options
|
|
55735
55791
|
- Users approve with \uD83D\uDC4D or deny with \uD83D\uDC4E by reacting to the message
|
|
@@ -55800,7 +55856,6 @@ var contextPromptFiles = new Map;
|
|
|
55800
55856
|
var log16 = createLogger("lifecycle");
|
|
55801
55857
|
var sessionLog3 = createSessionLog(log16);
|
|
55802
55858
|
var CHAT_PLATFORM_PROMPT = generateChatPlatformPrompt();
|
|
55803
|
-
|
|
55804
55859
|
// src/update-notifier.ts
|
|
55805
55860
|
var import_semver2 = __toESM(require_semver2(), 1);
|
|
55806
55861
|
|
|
@@ -55901,6 +55956,57 @@ ${code}
|
|
|
55901
55956
|
}
|
|
55902
55957
|
}
|
|
55903
55958
|
|
|
55959
|
+
// src/platform/mattermost/upload.ts
|
|
55960
|
+
import { readFile } from "fs/promises";
|
|
55961
|
+
var log22 = createLogger("mm-upload");
|
|
55962
|
+
async function uploadFileMattermost(args) {
|
|
55963
|
+
const { url: url2, token, channelId, threadId, filePath, filename, caption } = args;
|
|
55964
|
+
const buffer = await readFile(filePath);
|
|
55965
|
+
const uploadUrl = `${url2}/api/v4/files?channel_id=${encodeURIComponent(channelId)}`;
|
|
55966
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
55967
|
+
const formData = new FormData;
|
|
55968
|
+
formData.append("files", new Blob([arrayBuffer]), filename);
|
|
55969
|
+
log22.debug(`POST /files (${buffer.length} bytes, ${filename})`);
|
|
55970
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
55971
|
+
method: "POST",
|
|
55972
|
+
headers: {
|
|
55973
|
+
Authorization: `Bearer ${token}`
|
|
55974
|
+
},
|
|
55975
|
+
body: formData
|
|
55976
|
+
});
|
|
55977
|
+
if (!uploadResponse.ok) {
|
|
55978
|
+
const text = await uploadResponse.text();
|
|
55979
|
+
throw new Error(`Mattermost file upload failed: ${uploadResponse.status} ${text}`);
|
|
55980
|
+
}
|
|
55981
|
+
const uploadJson = await uploadResponse.json();
|
|
55982
|
+
const fileInfo = uploadJson.file_infos?.[0];
|
|
55983
|
+
if (!fileInfo?.id) {
|
|
55984
|
+
throw new Error("Mattermost file upload response missing file_infos[0].id");
|
|
55985
|
+
}
|
|
55986
|
+
const postUrl = `${url2}/api/v4/posts`;
|
|
55987
|
+
const postBody = {
|
|
55988
|
+
channel_id: channelId,
|
|
55989
|
+
message: caption ?? "",
|
|
55990
|
+
root_id: threadId,
|
|
55991
|
+
file_ids: [fileInfo.id]
|
|
55992
|
+
};
|
|
55993
|
+
log22.debug(`POST /posts (file_ids=[${fileInfo.id}])`);
|
|
55994
|
+
const postResponse = await fetch(postUrl, {
|
|
55995
|
+
method: "POST",
|
|
55996
|
+
headers: {
|
|
55997
|
+
Authorization: `Bearer ${token}`,
|
|
55998
|
+
"Content-Type": "application/json"
|
|
55999
|
+
},
|
|
56000
|
+
body: JSON.stringify(postBody)
|
|
56001
|
+
});
|
|
56002
|
+
if (!postResponse.ok) {
|
|
56003
|
+
const text = await postResponse.text();
|
|
56004
|
+
throw new Error(`Mattermost post-with-file failed: ${postResponse.status} ${text}`);
|
|
56005
|
+
}
|
|
56006
|
+
const post2 = await postResponse.json();
|
|
56007
|
+
return { postId: post2.id, fileId: fileInfo.id, post: post2 };
|
|
56008
|
+
}
|
|
56009
|
+
|
|
55904
56010
|
// src/platform/mattermost/permission-api.ts
|
|
55905
56011
|
var apiLog = createLogger("mm-api");
|
|
55906
56012
|
async function mattermostApi(config3, method, path2, body) {
|
|
@@ -56095,6 +56201,20 @@ class MattermostPermissionApi {
|
|
|
56095
56201
|
};
|
|
56096
56202
|
});
|
|
56097
56203
|
}
|
|
56204
|
+
async uploadFile(filePath, threadId, options) {
|
|
56205
|
+
const filename = sanitizeFilename(options?.filename ?? filePath);
|
|
56206
|
+
mcpLogger.debug(`uploadFile: ${filename} → thread ${formatShortId(threadId)}`);
|
|
56207
|
+
const result = await uploadFileMattermost({
|
|
56208
|
+
url: this.config.url,
|
|
56209
|
+
token: this.config.token,
|
|
56210
|
+
channelId: this.config.channelId,
|
|
56211
|
+
threadId,
|
|
56212
|
+
filePath,
|
|
56213
|
+
filename,
|
|
56214
|
+
caption: options?.caption
|
|
56215
|
+
});
|
|
56216
|
+
return { postId: result.postId };
|
|
56217
|
+
}
|
|
56098
56218
|
}
|
|
56099
56219
|
function createMattermostPermissionApi(config3) {
|
|
56100
56220
|
return new MattermostPermissionApi(config3);
|
|
@@ -56169,6 +56289,77 @@ ${code}
|
|
|
56169
56289
|
}
|
|
56170
56290
|
}
|
|
56171
56291
|
|
|
56292
|
+
// src/platform/slack/upload.ts
|
|
56293
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
56294
|
+
var log23 = createLogger("slack-upload");
|
|
56295
|
+
var DEFAULT_API_URL = "https://slack.com/api";
|
|
56296
|
+
async function uploadFileSlack(args) {
|
|
56297
|
+
const { botToken, channelId, threadTs, filePath, filename, caption } = args;
|
|
56298
|
+
const apiUrl = args.apiUrl ?? DEFAULT_API_URL;
|
|
56299
|
+
const buffer = await readFile2(filePath);
|
|
56300
|
+
const params = new URLSearchParams({ filename, length: String(buffer.length) });
|
|
56301
|
+
const step1Url = `${apiUrl}/files.getUploadURLExternal?${params.toString()}`;
|
|
56302
|
+
log23.debug(`GET files.getUploadURLExternal (${buffer.length} bytes, ${filename})`);
|
|
56303
|
+
const step1Response = await fetch(step1Url, {
|
|
56304
|
+
method: "GET",
|
|
56305
|
+
headers: {
|
|
56306
|
+
Authorization: `Bearer ${botToken}`
|
|
56307
|
+
}
|
|
56308
|
+
});
|
|
56309
|
+
if (!step1Response.ok) {
|
|
56310
|
+
const text = await step1Response.text();
|
|
56311
|
+
throw new Error(`Slack getUploadURLExternal failed: ${step1Response.status} ${text}`);
|
|
56312
|
+
}
|
|
56313
|
+
const step1Data = await step1Response.json();
|
|
56314
|
+
if (!step1Data.ok || !step1Data.upload_url || !step1Data.file_id) {
|
|
56315
|
+
throw new Error(`Slack getUploadURLExternal error: ${step1Data.error || "missing upload_url/file_id"}`);
|
|
56316
|
+
}
|
|
56317
|
+
const uploadUrl = step1Data.upload_url;
|
|
56318
|
+
const fileId = step1Data.file_id;
|
|
56319
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
56320
|
+
log23.debug(`POST <upload_url>`);
|
|
56321
|
+
const step2Response = await fetch(uploadUrl, {
|
|
56322
|
+
method: "POST",
|
|
56323
|
+
headers: {
|
|
56324
|
+
"Content-Type": "application/octet-stream"
|
|
56325
|
+
},
|
|
56326
|
+
body: arrayBuffer
|
|
56327
|
+
});
|
|
56328
|
+
if (!step2Response.ok) {
|
|
56329
|
+
const text = await step2Response.text();
|
|
56330
|
+
throw new Error(`Slack file bytes upload failed: ${step2Response.status} ${text}`);
|
|
56331
|
+
}
|
|
56332
|
+
const step3Body = {
|
|
56333
|
+
files: [{ id: fileId, title: caption ?? filename }],
|
|
56334
|
+
channel_id: channelId,
|
|
56335
|
+
thread_ts: threadTs
|
|
56336
|
+
};
|
|
56337
|
+
if (caption !== undefined) {
|
|
56338
|
+
step3Body.initial_comment = caption;
|
|
56339
|
+
}
|
|
56340
|
+
log23.debug(`POST files.completeUploadExternal (file_id=${fileId}, thread_ts=${threadTs})`);
|
|
56341
|
+
const step3Response = await fetch(`${apiUrl}/files.completeUploadExternal`, {
|
|
56342
|
+
method: "POST",
|
|
56343
|
+
headers: {
|
|
56344
|
+
Authorization: `Bearer ${botToken}`,
|
|
56345
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
56346
|
+
},
|
|
56347
|
+
body: JSON.stringify(step3Body)
|
|
56348
|
+
});
|
|
56349
|
+
if (!step3Response.ok) {
|
|
56350
|
+
const text = await step3Response.text();
|
|
56351
|
+
throw new Error(`Slack completeUploadExternal failed: ${step3Response.status} ${text}`);
|
|
56352
|
+
}
|
|
56353
|
+
const step3Data = await step3Response.json();
|
|
56354
|
+
if (!step3Data.ok) {
|
|
56355
|
+
throw new Error(`Slack completeUploadExternal error: ${step3Data.error || "unknown"}`);
|
|
56356
|
+
}
|
|
56357
|
+
if (!step3Data.ts) {
|
|
56358
|
+
log23.warn(`Slack completeUploadExternal returned no ts; using fileId ${fileId} as postId. ` + `Do not use this id for updatePost/addReaction.`);
|
|
56359
|
+
}
|
|
56360
|
+
return { fileId, postId: step3Data.ts ?? fileId };
|
|
56361
|
+
}
|
|
56362
|
+
|
|
56172
56363
|
// src/platform/slack/permission-api.ts
|
|
56173
56364
|
var SLACK_API_BASE = "https://slack.com/api";
|
|
56174
56365
|
async function slackApi(method, token, body) {
|
|
@@ -56377,6 +56568,19 @@ class SlackPermissionApi {
|
|
|
56377
56568
|
mcpLogger.debug("Got Socket Mode URL");
|
|
56378
56569
|
return response.url;
|
|
56379
56570
|
}
|
|
56571
|
+
async uploadFile(filePath, threadId, options) {
|
|
56572
|
+
const filename = sanitizeFilename(options?.filename ?? filePath);
|
|
56573
|
+
mcpLogger.debug(`uploadFile: ${filename} → thread_ts ${threadId}`);
|
|
56574
|
+
const result = await uploadFileSlack({
|
|
56575
|
+
botToken: this.config.botToken,
|
|
56576
|
+
channelId: this.config.channelId,
|
|
56577
|
+
threadTs: threadId,
|
|
56578
|
+
filePath,
|
|
56579
|
+
filename,
|
|
56580
|
+
caption: options?.caption
|
|
56581
|
+
});
|
|
56582
|
+
return { postId: result.postId };
|
|
56583
|
+
}
|
|
56380
56584
|
}
|
|
56381
56585
|
function createSlackPermissionApi(config3) {
|
|
56382
56586
|
return new SlackPermissionApi(config3);
|
|
@@ -56394,6 +56598,109 @@ function createPermissionApi(platformType, config3) {
|
|
|
56394
56598
|
}
|
|
56395
56599
|
}
|
|
56396
56600
|
|
|
56601
|
+
// src/mcp/path-validator.ts
|
|
56602
|
+
import { lstat, realpath, stat } from "fs/promises";
|
|
56603
|
+
import { sep as sep2, isAbsolute } from "path";
|
|
56604
|
+
function isUnderRoot(needle, root) {
|
|
56605
|
+
if (needle === root)
|
|
56606
|
+
return true;
|
|
56607
|
+
const withSep = root.endsWith(sep2) ? root : root + sep2;
|
|
56608
|
+
return needle.startsWith(withSep);
|
|
56609
|
+
}
|
|
56610
|
+
var DANGEROUSLY_WIDE_ROOTS = new Set([
|
|
56611
|
+
"/",
|
|
56612
|
+
"/home",
|
|
56613
|
+
"/Users",
|
|
56614
|
+
"/root",
|
|
56615
|
+
"/etc",
|
|
56616
|
+
"/var",
|
|
56617
|
+
"/tmp",
|
|
56618
|
+
"/usr",
|
|
56619
|
+
"/opt"
|
|
56620
|
+
]);
|
|
56621
|
+
function isDangerouslyWide(root) {
|
|
56622
|
+
if (DANGEROUSLY_WIDE_ROOTS.has(root))
|
|
56623
|
+
return true;
|
|
56624
|
+
if (root === "/private/tmp" || root === "/private/var" || root === "/private/etc")
|
|
56625
|
+
return true;
|
|
56626
|
+
return false;
|
|
56627
|
+
}
|
|
56628
|
+
async function validateOutboundPath(inputPath, opts) {
|
|
56629
|
+
if (typeof inputPath !== "string" || inputPath.length === 0) {
|
|
56630
|
+
return { ok: false, reason: "path is required" };
|
|
56631
|
+
}
|
|
56632
|
+
if (!isAbsolute(inputPath)) {
|
|
56633
|
+
return { ok: false, reason: "path must be absolute" };
|
|
56634
|
+
}
|
|
56635
|
+
if (!Number.isFinite(opts.maxBytes) || opts.maxBytes <= 0) {
|
|
56636
|
+
return {
|
|
56637
|
+
ok: false,
|
|
56638
|
+
reason: `invalid maxBytes ${opts.maxBytes} — check outboundFiles.maxBytes configuration`
|
|
56639
|
+
};
|
|
56640
|
+
}
|
|
56641
|
+
for (const root of opts.allowedRoots) {
|
|
56642
|
+
if (isDangerouslyWide(root)) {
|
|
56643
|
+
return {
|
|
56644
|
+
ok: false,
|
|
56645
|
+
reason: `refusing to validate against dangerously wide allowed root '${root}' — check SESSION_WORKING_DIR configuration`
|
|
56646
|
+
};
|
|
56647
|
+
}
|
|
56648
|
+
}
|
|
56649
|
+
let resolvedPath;
|
|
56650
|
+
try {
|
|
56651
|
+
resolvedPath = await realpath(inputPath);
|
|
56652
|
+
} catch (err) {
|
|
56653
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
56654
|
+
return { ok: false, reason: `cannot resolve path: ${msg}` };
|
|
56655
|
+
}
|
|
56656
|
+
const inAllowedRoot = opts.allowedRoots.some((root) => isUnderRoot(resolvedPath, root));
|
|
56657
|
+
if (!inAllowedRoot) {
|
|
56658
|
+
return {
|
|
56659
|
+
ok: false,
|
|
56660
|
+
reason: "path is outside the session working directory. Move the file into the working directory and retry."
|
|
56661
|
+
};
|
|
56662
|
+
}
|
|
56663
|
+
let lstatPre, statResolved;
|
|
56664
|
+
try {
|
|
56665
|
+
lstatPre = await lstat(inputPath);
|
|
56666
|
+
} catch (err) {
|
|
56667
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
56668
|
+
return { ok: false, reason: `cannot stat path: ${msg}` };
|
|
56669
|
+
}
|
|
56670
|
+
try {
|
|
56671
|
+
statResolved = await stat(resolvedPath);
|
|
56672
|
+
} catch (err) {
|
|
56673
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
56674
|
+
return { ok: false, reason: `cannot stat resolved path: ${msg}` };
|
|
56675
|
+
}
|
|
56676
|
+
if (!statResolved.isFile()) {
|
|
56677
|
+
return { ok: false, reason: "not a regular file" };
|
|
56678
|
+
}
|
|
56679
|
+
if (!lstatPre.isFile() && !lstatPre.isSymbolicLink()) {
|
|
56680
|
+
return { ok: false, reason: "not a regular file" };
|
|
56681
|
+
}
|
|
56682
|
+
const SUID = 2048;
|
|
56683
|
+
const SGID = 1024;
|
|
56684
|
+
if (statResolved.mode & SUID || statResolved.mode & SGID) {
|
|
56685
|
+
return { ok: false, reason: "refusing to upload SUID/SGID file" };
|
|
56686
|
+
}
|
|
56687
|
+
if (statResolved.size === 0) {
|
|
56688
|
+
return { ok: false, reason: "file is empty" };
|
|
56689
|
+
}
|
|
56690
|
+
if (statResolved.size > opts.maxBytes) {
|
|
56691
|
+
return {
|
|
56692
|
+
ok: false,
|
|
56693
|
+
reason: `file too large (${formatBytes(statResolved.size)} > ${formatBytes(opts.maxBytes)} limit)`
|
|
56694
|
+
};
|
|
56695
|
+
}
|
|
56696
|
+
return {
|
|
56697
|
+
ok: true,
|
|
56698
|
+
resolvedPath,
|
|
56699
|
+
size: statResolved.size,
|
|
56700
|
+
basename: sanitizeFilename(resolvedPath)
|
|
56701
|
+
};
|
|
56702
|
+
}
|
|
56703
|
+
|
|
56397
56704
|
// src/mcp/permission-server.ts
|
|
56398
56705
|
var PLATFORM_TYPE = process.env.PLATFORM_TYPE || "";
|
|
56399
56706
|
var PLATFORM_URL = process.env.PLATFORM_URL || "";
|
|
@@ -56402,6 +56709,11 @@ var PLATFORM_CHANNEL_ID = process.env.PLATFORM_CHANNEL_ID || "";
|
|
|
56402
56709
|
var PLATFORM_THREAD_ID = process.env.PLATFORM_THREAD_ID || "";
|
|
56403
56710
|
var ALLOWED_USERS = (process.env.ALLOWED_USERS || "").split(",").map((u) => u.trim()).filter((u) => u.length > 0);
|
|
56404
56711
|
var PERMISSION_TIMEOUT_MS = parseInt(process.env.PERMISSION_TIMEOUT_MS || "120000", 10);
|
|
56712
|
+
var SESSION_WORKING_DIR = process.env[OUTBOUND_ENV.SESSION_WORKING_DIR] || "";
|
|
56713
|
+
var SESSION_UPLOAD_DIR = process.env[OUTBOUND_ENV.SESSION_UPLOAD_DIR] || "";
|
|
56714
|
+
var OUTBOUND_FILES_ENABLED = (process.env[OUTBOUND_ENV.OUTBOUND_FILES_ENABLED] ?? "1") !== "0";
|
|
56715
|
+
var OUTBOUND_FILES_MAX_BYTES = parseInt(process.env[OUTBOUND_ENV.OUTBOUND_FILES_MAX_BYTES] || String(100 * 1024 * 1024), 10);
|
|
56716
|
+
var SEND_FILE_TOOL_NAME = "mcp__claude-threads-permissions__send_file";
|
|
56405
56717
|
var apiConfig = PLATFORM_TYPE === "slack" ? {
|
|
56406
56718
|
platformType: "slack",
|
|
56407
56719
|
botToken: PLATFORM_TOKEN,
|
|
@@ -56429,6 +56741,10 @@ function getApi() {
|
|
|
56429
56741
|
var allowAllSession = false;
|
|
56430
56742
|
async function handlePermissionWith(toolName, toolInput, cfg) {
|
|
56431
56743
|
mcpLogger.debug(`handlePermission called for ${toolName}`);
|
|
56744
|
+
if (toolName === SEND_FILE_TOOL_NAME) {
|
|
56745
|
+
mcpLogger.debug(`Auto-allowing ${toolName} (path validator is the real gate)`);
|
|
56746
|
+
return { behavior: "allow", updatedInput: toolInput };
|
|
56747
|
+
}
|
|
56432
56748
|
if (cfg.getAllowAll()) {
|
|
56433
56749
|
mcpLogger.debug(`Auto-allowing ${toolName} (allow all active)`);
|
|
56434
56750
|
return { behavior: "allow", updatedInput: toolInput };
|
|
@@ -56518,6 +56834,51 @@ var permissionInputSchema = {
|
|
|
56518
56834
|
tool_name: exports_external.string().describe("Name of the tool requesting permission"),
|
|
56519
56835
|
input: exports_external.record(exports_external.string(), exports_external.unknown()).describe("Tool input parameters")
|
|
56520
56836
|
};
|
|
56837
|
+
var sendFileInputSchema = {
|
|
56838
|
+
path: exports_external.string().describe("Absolute path of a file inside the session working directory. The bot will upload it to chat."),
|
|
56839
|
+
caption: exports_external.string().optional().describe("Optional message body / initial comment shown alongside the file.")
|
|
56840
|
+
};
|
|
56841
|
+
async function handleSendFileWith(args, cfg) {
|
|
56842
|
+
if (!cfg.enabled) {
|
|
56843
|
+
return { ok: false, reason: "outbound file sending is disabled by the operator" };
|
|
56844
|
+
}
|
|
56845
|
+
if (!cfg.api.uploadFile) {
|
|
56846
|
+
return { ok: false, reason: "this platform does not support outbound file uploads" };
|
|
56847
|
+
}
|
|
56848
|
+
if (!cfg.threadId) {
|
|
56849
|
+
return { ok: false, reason: "no thread context — file uploads only work inside a session thread" };
|
|
56850
|
+
}
|
|
56851
|
+
if (cfg.allowedRoots.length === 0) {
|
|
56852
|
+
return { ok: false, reason: "no allowed roots configured for outbound file uploads" };
|
|
56853
|
+
}
|
|
56854
|
+
const validated = await validateOutboundPath(args.path, {
|
|
56855
|
+
allowedRoots: cfg.allowedRoots,
|
|
56856
|
+
maxBytes: cfg.maxBytes
|
|
56857
|
+
});
|
|
56858
|
+
if (!validated.ok) {
|
|
56859
|
+
return { ok: false, reason: validated.reason };
|
|
56860
|
+
}
|
|
56861
|
+
try {
|
|
56862
|
+
const result = await cfg.api.uploadFile(validated.resolvedPath, cfg.threadId, {
|
|
56863
|
+
caption: args.caption,
|
|
56864
|
+
filename: validated.basename
|
|
56865
|
+
});
|
|
56866
|
+
return { ok: true, postId: result.postId };
|
|
56867
|
+
} catch (err) {
|
|
56868
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
56869
|
+
mcpLogger.warn(`send_file upload failed: ${reason}`);
|
|
56870
|
+
return { ok: false, reason };
|
|
56871
|
+
}
|
|
56872
|
+
}
|
|
56873
|
+
async function handleSendFile(args) {
|
|
56874
|
+
return handleSendFileWith(args, {
|
|
56875
|
+
api: getApi(),
|
|
56876
|
+
threadId: PLATFORM_THREAD_ID,
|
|
56877
|
+
enabled: OUTBOUND_FILES_ENABLED,
|
|
56878
|
+
allowedRoots: [SESSION_WORKING_DIR, SESSION_UPLOAD_DIR].filter((p) => p.length > 0),
|
|
56879
|
+
maxBytes: OUTBOUND_FILES_MAX_BYTES
|
|
56880
|
+
});
|
|
56881
|
+
}
|
|
56521
56882
|
async function main() {
|
|
56522
56883
|
const server = new McpServer({
|
|
56523
56884
|
name: "claude-threads-permissions",
|
|
@@ -56529,6 +56890,12 @@ async function main() {
|
|
|
56529
56890
|
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
56530
56891
|
};
|
|
56531
56892
|
});
|
|
56893
|
+
server.tool("send_file", "Send a file from the session working directory directly into the chat thread. " + "Use this when the user asked to receive a file inline, or when you produce an artifact " + "they should see (screenshot, generated audio, plot, document). The path must be absolute " + "and inside the session working directory. Returns { ok: true, postId } on success or " + "{ ok: false, reason } on failure.", sendFileInputSchema, async ({ path: path2, caption }) => {
|
|
56894
|
+
const result = await handleSendFile({ path: path2, caption });
|
|
56895
|
+
return {
|
|
56896
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
56897
|
+
};
|
|
56898
|
+
});
|
|
56532
56899
|
const transport = new StdioServerTransport;
|
|
56533
56900
|
await server.connect(transport);
|
|
56534
56901
|
mcpLogger.info(`Permission server ready (platform: ${PLATFORM_TYPE})`);
|
|
@@ -56538,5 +56905,6 @@ main().catch((err) => {
|
|
|
56538
56905
|
process.exit(1);
|
|
56539
56906
|
});
|
|
56540
56907
|
export {
|
|
56908
|
+
handleSendFileWith,
|
|
56541
56909
|
handlePermissionWith
|
|
56542
56910
|
};
|