@forestadmin/mcp-server 1.21.0 → 1.22.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/README.md +247 -0
- package/dist/cli.js +36 -17
- package/dist/file-uploads/ephemeral-storage.d.ts +46 -0
- package/dist/file-uploads/ephemeral-storage.js +216 -0
- package/dist/file-uploads/file-reference.d.ts +13 -0
- package/dist/file-uploads/file-reference.js +21 -0
- package/dist/file-uploads/handles.d.ts +11 -0
- package/dist/file-uploads/handles.js +41 -0
- package/dist/file-uploads/resolve.d.ts +8 -0
- package/dist/file-uploads/resolve.js +145 -0
- package/dist/file-uploads/semaphore.d.ts +3 -0
- package/dist/file-uploads/semaphore.js +37 -0
- package/dist/file-uploads/types.d.ts +74 -0
- package/dist/file-uploads/types.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/server.d.ts +15 -1
- package/dist/server.js +73 -2
- package/dist/tool-context.d.ts +2 -0
- package/dist/tools/execute-action.js +12 -4
- package/dist/tools/get-action-form.js +37 -6
- package/dist/tools/request-action-file-upload.d.ts +4 -0
- package/dist/tools/request-action-file-upload.js +162 -0
- package/dist/utils/load-file-uploads.d.ts +14 -0
- package/dist/utils/load-file-uploads.js +105 -0
- package/package.json +3 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UPLOADED_FILE_PREFIX = void 0;
|
|
4
|
+
exports.default = parseFileReference;
|
|
5
|
+
/**
|
|
6
|
+
* Sentinel prefix used inside action form values, e.g.
|
|
7
|
+
* { "document": "$uploadedFile:<jwt>" }
|
|
8
|
+
* A string rather than an object, so it passes the agent-client field validation and stays
|
|
9
|
+
* cheap when getActionForm echoes it back into the model's context.
|
|
10
|
+
*/
|
|
11
|
+
exports.UPLOADED_FILE_PREFIX = '$uploadedFile:';
|
|
12
|
+
/**
|
|
13
|
+
* Isolated so that the file URIs the MCP specification is designing (SEP-2631) can be recognized
|
|
14
|
+
* here without touching the resolution path.
|
|
15
|
+
*/
|
|
16
|
+
function parseFileReference(value) {
|
|
17
|
+
if (typeof value !== 'string' || !value.startsWith(exports.UPLOADED_FILE_PREFIX))
|
|
18
|
+
return null;
|
|
19
|
+
return value.slice(exports.UPLOADED_FILE_PREFIX.length);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmlsZS1yZWZlcmVuY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZmlsZS11cGxvYWRzL2ZpbGUtcmVmZXJlbmNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQVlBLHFDQUlDO0FBaEJEOzs7OztHQUtHO0FBQ1UsUUFBQSxvQkFBb0IsR0FBRyxnQkFBZ0IsQ0FBQztBQUVyRDs7O0dBR0c7QUFDSCxTQUF3QixrQkFBa0IsQ0FBQyxLQUFjO0lBQ3ZELElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLENBQUMsS0FBSyxDQUFDLFVBQVUsQ0FBQyw0QkFBb0IsQ0FBQztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBRXRGLE9BQU8sS0FBSyxDQUFDLEtBQUssQ0FBQyw0QkFBb0IsQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUNsRCxDQUFDIn0=
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface UploadHandleClaims {
|
|
2
|
+
key: string;
|
|
3
|
+
name: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
sha256Base64?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function signUploadHandle(claims: UploadHandleClaims & {
|
|
8
|
+
userId: number | string;
|
|
9
|
+
}, authSecret: string, ttlSeconds: number): string;
|
|
10
|
+
export declare function verifyUploadHandle(handle: string, userId: number | string, authSecret: string): UploadHandleClaims;
|
|
11
|
+
//# sourceMappingURL=handles.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
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.signUploadHandle = signUploadHandle;
|
|
7
|
+
exports.verifyUploadHandle = verifyUploadHandle;
|
|
8
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
9
|
+
const HANDLE_TYPE = 'mcp-upload';
|
|
10
|
+
function signUploadHandle(claims, authSecret, ttlSeconds) {
|
|
11
|
+
return jsonwebtoken_1.default.sign({
|
|
12
|
+
type: HANDLE_TYPE,
|
|
13
|
+
key: claims.key,
|
|
14
|
+
name: claims.name,
|
|
15
|
+
mime: claims.mimeType,
|
|
16
|
+
uploader: String(claims.userId),
|
|
17
|
+
...(claims.sha256Base64 && { sha256: claims.sha256Base64 }),
|
|
18
|
+
}, authSecret, { expiresIn: ttlSeconds });
|
|
19
|
+
}
|
|
20
|
+
function verifyUploadHandle(handle, userId, authSecret) {
|
|
21
|
+
const decoded = jsonwebtoken_1.default.verify(handle, authSecret, { algorithms: ['HS256'] });
|
|
22
|
+
// The handle is signed with the secret that also signs access tokens, so the type claim is
|
|
23
|
+
// what keeps an access token from being redeemed as an upload handle.
|
|
24
|
+
if (decoded?.type !== HANDLE_TYPE)
|
|
25
|
+
throw new Error('Not an upload handle');
|
|
26
|
+
if (decoded.uploader !== String(userId))
|
|
27
|
+
throw new Error('Handle was issued to another user');
|
|
28
|
+
// key feeds storage.download and mime lands in the stored file, so neither may be undefined.
|
|
29
|
+
if (typeof decoded.key !== 'string' ||
|
|
30
|
+
typeof decoded.name !== 'string' ||
|
|
31
|
+
typeof decoded.mime !== 'string') {
|
|
32
|
+
throw new Error('Malformed upload handle');
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
key: decoded.key,
|
|
36
|
+
name: decoded.name,
|
|
37
|
+
mimeType: decoded.mime,
|
|
38
|
+
sha256Base64: decoded.sha256,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGFuZGxlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9maWxlLXVwbG9hZHMvaGFuZGxlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQVdBLDRDQWlCQztBQUVELGdEQWtDQztBQWhFRCxnRUFBd0M7QUFFeEMsTUFBTSxXQUFXLEdBQUcsWUFBWSxDQUFDO0FBU2pDLFNBQWdCLGdCQUFnQixDQUM5QixNQUF3RCxFQUN4RCxVQUFrQixFQUNsQixVQUFrQjtJQUVsQixPQUFPLHNCQUFZLENBQUMsSUFBSSxDQUN0QjtRQUNFLElBQUksRUFBRSxXQUFXO1FBQ2pCLEdBQUcsRUFBRSxNQUFNLENBQUMsR0FBRztRQUNmLElBQUksRUFBRSxNQUFNLENBQUMsSUFBSTtRQUNqQixJQUFJLEVBQUUsTUFBTSxDQUFDLFFBQVE7UUFDckIsUUFBUSxFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDO1FBQy9CLEdBQUcsQ0FBQyxNQUFNLENBQUMsWUFBWSxJQUFJLEVBQUUsTUFBTSxFQUFFLE1BQU0sQ0FBQyxZQUFZLEVBQUUsQ0FBQztLQUM1RCxFQUNELFVBQVUsRUFDVixFQUFFLFNBQVMsRUFBRSxVQUFVLEVBQUUsQ0FDMUIsQ0FBQztBQUNKLENBQUM7QUFFRCxTQUFnQixrQkFBa0IsQ0FDaEMsTUFBYyxFQUNkLE1BQXVCLEVBQ3ZCLFVBQWtCO0lBRWxCLE1BQU0sT0FBTyxHQUFHLHNCQUFZLENBQUMsTUFBTSxDQUFDLE1BQU0sRUFBRSxVQUFVLEVBQUUsRUFBRSxVQUFVLEVBQUUsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQU9oRixDQUFDO0lBRUYsMkZBQTJGO0lBQzNGLHNFQUFzRTtJQUN0RSxJQUFJLE9BQU8sRUFBRSxJQUFJLEtBQUssV0FBVztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsc0JBQXNCLENBQUMsQ0FBQztJQUMzRSxJQUFJLE9BQU8sQ0FBQyxRQUFRLEtBQUssTUFBTSxDQUFDLE1BQU0sQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsbUNBQW1DLENBQUMsQ0FBQztJQUU5Riw2RkFBNkY7SUFDN0YsSUFDRSxPQUFPLE9BQU8sQ0FBQyxHQUFHLEtBQUssUUFBUTtRQUMvQixPQUFPLE9BQU8sQ0FBQyxJQUFJLEtBQUssUUFBUTtRQUNoQyxPQUFPLE9BQU8sQ0FBQyxJQUFJLEtBQUssUUFBUSxFQUNoQyxDQUFDO1FBQ0QsTUFBTSxJQUFJLEtBQUssQ0FBQyx5QkFBeUIsQ0FBQyxDQUFDO0lBQzdDLENBQUM7SUFFRCxPQUFPO1FBQ0wsR0FBRyxFQUFFLE9BQU8sQ0FBQyxHQUFHO1FBQ2hCLElBQUksRUFBRSxPQUFPLENBQUMsSUFBSTtRQUNsQixRQUFRLEVBQUUsT0FBTyxDQUFDLElBQUk7UUFDdEIsWUFBWSxFQUFFLE9BQU8sQ0FBQyxNQUFNO0tBQzdCLENBQUM7QUFDSixDQUFDIn0=
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResolvedFileUploads } from './types';
|
|
2
|
+
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Only executeAction resolves references. getActionForm echoes field values back to the model,
|
|
5
|
+
* and a resolved file there would put the content back into the model's context.
|
|
6
|
+
*/
|
|
7
|
+
export default function resolveUploadedFileValues(values: Record<string, unknown>, authInfo: AuthInfo | undefined, uploads: ResolvedFileUploads | undefined): Promise<Record<string, unknown>>;
|
|
8
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.default = resolveUploadedFileValues;
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const file_reference_1 = __importDefault(require("./file-reference"));
|
|
42
|
+
const handles_1 = require("./handles");
|
|
43
|
+
// Keyed by reference so one used by several fields is downloaded once. The field is the first
|
|
44
|
+
// one that mentioned it, which is what error messages name.
|
|
45
|
+
function collectReferences(values) {
|
|
46
|
+
const references = new Map();
|
|
47
|
+
for (const [field, value] of Object.entries(values)) {
|
|
48
|
+
const candidates = Array.isArray(value) ? value : [value];
|
|
49
|
+
candidates.forEach(candidate => {
|
|
50
|
+
const handle = (0, file_reference_1.default)(candidate);
|
|
51
|
+
if (handle && !references.has(candidate)) {
|
|
52
|
+
references.set(candidate, { field, handle });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return references;
|
|
57
|
+
}
|
|
58
|
+
// The contract puts no bound on a storage read. Without this, a backend that stops answering
|
|
59
|
+
// holds its concurrency slot forever while the caller's own request timeout fires, so the
|
|
60
|
+
// failure would be invisible here and the slots would drain away one by one.
|
|
61
|
+
function withTimeout(operation, seconds, promise) {
|
|
62
|
+
let timer;
|
|
63
|
+
return Promise.race([
|
|
64
|
+
promise,
|
|
65
|
+
new Promise((_, reject) => {
|
|
66
|
+
timer = setTimeout(() => reject(new Error(`${operation} timed out after ${seconds}s`)), seconds * 1000);
|
|
67
|
+
}),
|
|
68
|
+
]).finally(() => clearTimeout(timer));
|
|
69
|
+
}
|
|
70
|
+
async function download(field, claims, uploads) {
|
|
71
|
+
const tooLarge = (bytes) => new Error(`Field "${field}": uploaded file is ${bytes} bytes, above the ${uploads.maxBytes} byte limit`);
|
|
72
|
+
// A pre-authorized upload URL cannot always cap the object size, so the limit is enforced here,
|
|
73
|
+
// before the bytes are read whenever the backend can report a size. Wrapped like the download
|
|
74
|
+
// below: a backend that reports size from object metadata rejects on a missing key, which is the
|
|
75
|
+
// likeliest failure of the whole flow — the upload was blocked. Raw, that reaches the model as a
|
|
76
|
+
// bare SDK string with no field name and no diagnosis.
|
|
77
|
+
const size = await withTimeout(`Field "${field}": reading the size of the uploaded file`, uploads.downloadTimeoutSeconds, uploads.storage.getSize(claims.key)).catch((error) => {
|
|
78
|
+
throw new Error(`Field "${field}": could not read the uploaded file. ` +
|
|
79
|
+
`Did the upload to uploadUrl succeed? (${error.message})`);
|
|
80
|
+
});
|
|
81
|
+
if (typeof size === 'number' && Number.isFinite(size) && size > uploads.maxBytes) {
|
|
82
|
+
throw tooLarge(size);
|
|
83
|
+
}
|
|
84
|
+
// A backend that honours the contract rejects a missing object, so this is the shape the most
|
|
85
|
+
// likely mistake takes: the handle was never uploaded to. The raw message alone reads as an
|
|
86
|
+
// infrastructure failure. The key it may contain is already in the model's context, inside the
|
|
87
|
+
// handle it just sent.
|
|
88
|
+
const buffer = await withTimeout(`Field "${field}": reading the uploaded file`, uploads.downloadTimeoutSeconds, uploads.storage.download(claims.key)).catch((error) => {
|
|
89
|
+
throw new Error(`Field "${field}": could not read the uploaded file. ` +
|
|
90
|
+
`Did the upload to uploadUrl succeed? (${error.message})`);
|
|
91
|
+
});
|
|
92
|
+
// Re-checked after download because getSize is advisory, and because the object can be
|
|
93
|
+
// replaced between the two calls.
|
|
94
|
+
if (buffer.length > uploads.maxBytes)
|
|
95
|
+
throw tooLarge(buffer.length);
|
|
96
|
+
// Even if the upload URL leaked and someone overwrote the object, content substituted after
|
|
97
|
+
// the client pinned a digest cannot be redeemed.
|
|
98
|
+
if (claims.sha256Base64) {
|
|
99
|
+
const digest = crypto.createHash('sha256').update(new Uint8Array(buffer)).digest('base64');
|
|
100
|
+
if (digest !== claims.sha256Base64) {
|
|
101
|
+
throw new Error(`Field "${field}": uploaded file does not match the sha256 it was pinned to`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { buffer, mimeType: claims.mimeType, name: claims.name };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Only executeAction resolves references. getActionForm echoes field values back to the model,
|
|
108
|
+
* and a resolved file there would put the content back into the model's context.
|
|
109
|
+
*/
|
|
110
|
+
async function resolveUploadedFileValues(values, authInfo, uploads) {
|
|
111
|
+
const references = collectReferences(values);
|
|
112
|
+
if (references.size === 0)
|
|
113
|
+
return values;
|
|
114
|
+
if (!uploads) {
|
|
115
|
+
throw new Error('File uploads are not configured on this server. ' +
|
|
116
|
+
'Ask the administrator to set the fileUploads option to enable action file fields.');
|
|
117
|
+
}
|
|
118
|
+
const userId = authInfo?.extra?.userId;
|
|
119
|
+
if (userId === undefined || userId === null) {
|
|
120
|
+
throw new Error('Cannot resolve uploaded files without an authenticated user');
|
|
121
|
+
}
|
|
122
|
+
// Verified before acquiring a download slot: it is pure CPU, and it gates everything
|
|
123
|
+
// expensive, so a batch of forged handles is rejected instead of queueing behind the limit.
|
|
124
|
+
const verified = [...references].map(([reference, { field, handle }]) => {
|
|
125
|
+
try {
|
|
126
|
+
return { field, reference, claims: (0, handles_1.verifyUploadHandle)(handle, userId, uploads.authSecret) };
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
// Reaching the model as a bare "jwt expired" names no field, and with several file fields in
|
|
130
|
+
// one form it names none of them.
|
|
131
|
+
throw new Error(`Field "${field}": this file handle is not usable (${error.message}). ` +
|
|
132
|
+
'Call requestActionFileUpload again and upload the file to the new destination.');
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const files = new Map(await Promise.all(verified.map(async ({ field, reference, claims }) => [
|
|
136
|
+
reference,
|
|
137
|
+
await uploads.limitDownload(() => download(field, claims, uploads)),
|
|
138
|
+
])));
|
|
139
|
+
const substitute = (value) => files.get(value) ?? value;
|
|
140
|
+
return Object.fromEntries(Object.entries(values).map(([field, value]) => [
|
|
141
|
+
field,
|
|
142
|
+
Array.isArray(value) ? value.map(substitute) : substitute(value),
|
|
143
|
+
]));
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVzb2x2ZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9maWxlLXVwbG9hZHMvcmVzb2x2ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQW1IQSw0Q0F3REM7QUF0S0QsK0NBQWlDO0FBRWpDLHNFQUFrRDtBQUNsRCx1Q0FBK0M7QUFFL0MsOEZBQThGO0FBQzlGLDREQUE0RDtBQUM1RCxTQUFTLGlCQUFpQixDQUN4QixNQUErQjtJQUUvQixNQUFNLFVBQVUsR0FBRyxJQUFJLEdBQUcsRUFBNkMsQ0FBQztJQUV4RSxLQUFLLE1BQU0sQ0FBQyxLQUFLLEVBQUUsS0FBSyxDQUFDLElBQUksTUFBTSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQ3BELE1BQU0sVUFBVSxHQUFHLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUUxRCxVQUFVLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxFQUFFO1lBQzdCLE1BQU0sTUFBTSxHQUFHLElBQUEsd0JBQWtCLEVBQUMsU0FBUyxDQUFDLENBQUM7WUFFN0MsSUFBSSxNQUFNLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLFNBQW1CLENBQUMsRUFBRSxDQUFDO2dCQUNuRCxVQUFVLENBQUMsR0FBRyxDQUFDLFNBQW1CLEVBQUUsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLENBQUMsQ0FBQztZQUN6RCxDQUFDO1FBQ0gsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDO0lBRUQsT0FBTyxVQUFVLENBQUM7QUFDcEIsQ0FBQztBQUVELDZGQUE2RjtBQUM3RiwwRkFBMEY7QUFDMUYsNkVBQTZFO0FBQzdFLFNBQVMsV0FBVyxDQUFJLFNBQWlCLEVBQUUsT0FBZSxFQUFFLE9BQW1CO0lBQzdFLElB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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = createSemaphore;
|
|
4
|
+
function createSemaphore(limit) {
|
|
5
|
+
let active = 0;
|
|
6
|
+
const queue = [];
|
|
7
|
+
// The queue is deliberately unbounded: one action submits all its file references at once, so a
|
|
8
|
+
// cap would reject a form carrying more of them than the limit allows, every time. Each read is
|
|
9
|
+
// bounded by downloadTimeoutSeconds instead, which is what keeps the queue draining.
|
|
10
|
+
const acquire = () => new Promise(resolve => {
|
|
11
|
+
if (active < limit) {
|
|
12
|
+
active += 1;
|
|
13
|
+
resolve();
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
queue.push(resolve);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const release = () => {
|
|
20
|
+
const next = queue.shift();
|
|
21
|
+
// The slot transfers to the waiting task, so active stays unchanged.
|
|
22
|
+
if (next)
|
|
23
|
+
next();
|
|
24
|
+
else
|
|
25
|
+
active -= 1;
|
|
26
|
+
};
|
|
27
|
+
return async (task) => {
|
|
28
|
+
await acquire();
|
|
29
|
+
try {
|
|
30
|
+
return await task();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
release();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VtYXBob3JlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ZpbGUtdXBsb2Fkcy9zZW1hcGhvcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFFQSxrQ0FrQ0M7QUFsQ0QsU0FBd0IsZUFBZSxDQUFDLEtBQWE7SUFDbkQsSUFBSSxNQUFNLEdBQUcsQ0FBQyxDQUFDO0lBQ2YsTUFBTSxLQUFLLEdBQXNCLEVBQUUsQ0FBQztJQUVwQyxnR0FBZ0c7SUFDaEcsZ0dBQWdHO0lBQ2hHLHFGQUFxRjtJQUNyRixNQUFNLE9BQU8sR0FBRyxHQUFHLEVBQUUsQ0FDbkIsSUFBSSxPQUFPLENBQU8sT0FBTyxDQUFDLEVBQUU7UUFDMUIsSUFBSSxNQUFNLEdBQUcsS0FBSyxFQUFFLENBQUM7WUFDbkIsTUFBTSxJQUFJLENBQUMsQ0FBQztZQUNaLE9BQU8sRUFBRSxDQUFDO1FBQ1osQ0FBQzthQUFNLENBQUM7WUFDTixLQUFLLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ3RCLENBQUM7SUFDSCxDQUFDLENBQUMsQ0FBQztJQUVMLE1BQU0sT0FBTyxHQUFHLEdBQUcsRUFBRTtRQUNuQixNQUFNLElBQUksR0FBRyxLQUFLLENBQUMsS0FBSyxFQUFFLENBQUM7UUFFM0IscUVBQXFFO1FBQ3JFLElBQUksSUFBSTtZQUFFLElBQUksRUFBRSxDQUFDOztZQUNaLE1BQU0sSUFBSSxDQUFDLENBQUM7SUFDbkIsQ0FBQyxDQUFDO0lBRUYsT0FBTyxLQUFLLEVBQUssSUFBc0IsRUFBYyxFQUFFO1FBQ3JELE1BQU0sT0FBTyxFQUFFLENBQUM7UUFFaEIsSUFBSSxDQUFDO1lBQ0gsT0FBTyxNQUFNLElBQUksRUFBRSxDQUFDO1FBQ3RCLENBQUM7Z0JBQVMsQ0FBQztZQUNULE9BQU8sRUFBRSxDQUFDO1FBQ1osQ0FBQztJQUNILENBQUMsQ0FBQztBQUNKLENBQUMifQ==
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { RunExclusive } from './semaphore';
|
|
2
|
+
import type { Logger } from '../server';
|
|
3
|
+
/** Storage backend for the action file upload side-channel. See the README for the flow. */
|
|
4
|
+
export interface UploadStorage {
|
|
5
|
+
/** The URL must be reachable by the MCP client, which is what uploads the bytes. */
|
|
6
|
+
createUploadUrl(params: {
|
|
7
|
+
key: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
/** Advisory: the server re-verifies the digest after download regardless. */
|
|
10
|
+
sha256?: string;
|
|
11
|
+
expiresInSeconds: number;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
url: string;
|
|
14
|
+
method?: string;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
}>;
|
|
17
|
+
/** Must reject when the object does not exist. */
|
|
18
|
+
download(key: string): Promise<Buffer>;
|
|
19
|
+
/**
|
|
20
|
+
* Size of the uploaded object, used to reject oversized uploads before downloading them.
|
|
21
|
+
* Return undefined when the backend cannot report it cheaply — `maxBytes` is then only
|
|
22
|
+
* enforced after the bytes are in memory, so the process holds the whole object either way.
|
|
23
|
+
*
|
|
24
|
+
* Rejecting is fine when the object is absent — reading its metadata is how most backends
|
|
25
|
+
* answer this, and a missing object is the likeliest outcome of the whole flow, the upload
|
|
26
|
+
* having been blocked. The rejection is reported to the caller with the field name and the
|
|
27
|
+
* message, so it does not have to be distinguished from an unreadable one here.
|
|
28
|
+
*/
|
|
29
|
+
getSize(key: string): Promise<number | undefined>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @experimental The MCP specification is still designing its own file transfer story
|
|
33
|
+
* (SEP-2631). The storage contract is expected to survive, but the `requestActionFileUpload` tool
|
|
34
|
+
* and the handle format may change to follow the specification once it lands.
|
|
35
|
+
*/
|
|
36
|
+
export interface FileUploadsOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Where uploaded objects live between the upload and the action. Omit it and the server keeps
|
|
39
|
+
* them in memory, on its own origin — nothing to provision, but only correct for a single
|
|
40
|
+
* instance: see the README.
|
|
41
|
+
*/
|
|
42
|
+
storage?: UploadStorage;
|
|
43
|
+
keyPrefix?: string;
|
|
44
|
+
uploadUrlTtlSeconds?: number;
|
|
45
|
+
/** Must stay longer than uploadUrlTtlSeconds, so a slow upload leaves time to run the action. */
|
|
46
|
+
handleTtlSeconds?: number;
|
|
47
|
+
maxBytes?: number;
|
|
48
|
+
maxConcurrentDownloads?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Total size the in-memory store holds across all pending uploads. Defaults to 64 MiB. Has no
|
|
51
|
+
* effect once a `storage` is given.
|
|
52
|
+
*/
|
|
53
|
+
ephemeralMaxTotalBytes?: number;
|
|
54
|
+
/**
|
|
55
|
+
* How long a single storage read may take. Keep it well under the request timeout of the
|
|
56
|
+
* clients calling the agent, so a slow backend fails here rather than being cut mid-flight.
|
|
57
|
+
* Defaults to 15 seconds.
|
|
58
|
+
*/
|
|
59
|
+
downloadTimeoutSeconds?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface ResolvedFileUploads {
|
|
62
|
+
storage: UploadStorage;
|
|
63
|
+
keyPrefix: string;
|
|
64
|
+
uploadUrlTtlSeconds: number;
|
|
65
|
+
handleTtlSeconds: number;
|
|
66
|
+
maxBytes: number;
|
|
67
|
+
downloadTimeoutSeconds: number;
|
|
68
|
+
ephemeralMaxTotalBytes: number;
|
|
69
|
+
authSecret: string;
|
|
70
|
+
limitDownload: RunExclusive;
|
|
71
|
+
}
|
|
72
|
+
export declare const DEFAULT_EPHEMERAL_MAX_TOTAL_BYTES: number;
|
|
73
|
+
export declare function resolveFileUploads(options: FileUploadsOptions | undefined, authSecret: string, logger?: Logger): ResolvedFileUploads | undefined;
|
|
74
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
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.DEFAULT_EPHEMERAL_MAX_TOTAL_BYTES = void 0;
|
|
7
|
+
exports.resolveFileUploads = resolveFileUploads;
|
|
8
|
+
const semaphore_1 = __importDefault(require("./semaphore"));
|
|
9
|
+
const DEFAULT_KEY_PREFIX = 'mcp-uploads/';
|
|
10
|
+
const DEFAULT_UPLOAD_URL_TTL_SECONDS = 15 * 60;
|
|
11
|
+
const DEFAULT_HANDLE_TTL_SECONDS = 45 * 60;
|
|
12
|
+
const DEFAULT_MAX_BYTES = 20 * 1024 * 1024;
|
|
13
|
+
const DEFAULT_MAX_CONCURRENT_DOWNLOADS = 5;
|
|
14
|
+
const DEFAULT_DOWNLOAD_TIMEOUT_SECONDS = 15;
|
|
15
|
+
// Deliberately absolute rather than a multiple of maxBytes: derived, raising the per-file limit
|
|
16
|
+
// would multiply what the process can hold, which is the opposite of what setting it suggests.
|
|
17
|
+
exports.DEFAULT_EPHEMERAL_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
18
|
+
function positiveInteger(field, value) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
22
|
+
throw new Error(`Invalid fileUploads.${field} "${value}": it must be a positive integer.`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function resolveFileUploads(options, authSecret, logger) {
|
|
27
|
+
if (!options)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (!options.storage)
|
|
30
|
+
throw new Error('fileUploads.storage is required.');
|
|
31
|
+
const uploadUrlTtlSeconds = positiveInteger('uploadUrlTtlSeconds', options.uploadUrlTtlSeconds) ??
|
|
32
|
+
DEFAULT_UPLOAD_URL_TTL_SECONDS;
|
|
33
|
+
const handleTtlSeconds = positiveInteger('handleTtlSeconds', options.handleTtlSeconds) ?? DEFAULT_HANDLE_TTL_SECONDS;
|
|
34
|
+
const maxBytes = positiveInteger('maxBytes', options.maxBytes) ?? DEFAULT_MAX_BYTES;
|
|
35
|
+
const ephemeralMaxTotalBytes = positiveInteger('ephemeralMaxTotalBytes', options.ephemeralMaxTotalBytes) ??
|
|
36
|
+
exports.DEFAULT_EPHEMERAL_MAX_TOTAL_BYTES;
|
|
37
|
+
// A handle expiring before its upload URL means uploads succeed and every redemption then
|
|
38
|
+
// fails with a bare "jwt expired".
|
|
39
|
+
if (handleTtlSeconds <= uploadUrlTtlSeconds) {
|
|
40
|
+
logger?.('Warn', `fileUploads.handleTtlSeconds=${handleTtlSeconds} is shorter than ` +
|
|
41
|
+
`fileUploads.uploadUrlTtlSeconds=${uploadUrlTtlSeconds}: a slow upload will finish with ` +
|
|
42
|
+
'an already expired handle.');
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
storage: options.storage,
|
|
46
|
+
keyPrefix: options.keyPrefix ?? DEFAULT_KEY_PREFIX,
|
|
47
|
+
uploadUrlTtlSeconds,
|
|
48
|
+
handleTtlSeconds,
|
|
49
|
+
maxBytes,
|
|
50
|
+
downloadTimeoutSeconds: positiveInteger('downloadTimeoutSeconds', options.downloadTimeoutSeconds) ??
|
|
51
|
+
DEFAULT_DOWNLOAD_TIMEOUT_SECONDS,
|
|
52
|
+
ephemeralMaxTotalBytes,
|
|
53
|
+
authSecret,
|
|
54
|
+
limitDownload: (0, semaphore_1.default)(positiveInteger('maxConcurrentDownloads', options.maxConcurrentDownloads) ??
|
|
55
|
+
DEFAULT_MAX_CONCURRENT_DOWNLOADS),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZmlsZS11cGxvYWRzL3R5cGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7OztBQWtHQSxnREErQ0M7QUE5SUQsNERBQTBDO0FBd0UxQyxNQUFNLGtCQUFrQixHQUFHLGNBQWMsQ0FBQztBQUMxQyxNQUFNLDhCQUE4QixHQUFHLEVBQUUsR0FBRyxFQUFFLENBQUM7QUFDL0MsTUFBTSwwQkFBMEIsR0FBRyxFQUFFLEdBQUcsRUFBRSxDQUFDO0FBQzNDLE1BQU0saUJBQWlCLEdBQUcsRUFBRSxHQUFHLElBQUksR0FBRyxJQUFJLENBQUM7QUFDM0MsTUFBTSxnQ0FBZ0MsR0FBRyxDQUFDLENBQUM7QUFDM0MsTUFBTSxnQ0FBZ0MsR0FBRyxFQUFFLENBQUM7QUFDNUMsZ0dBQWdHO0FBQ2hHLCtGQUErRjtBQUNsRixRQUFBLGlDQUFpQyxHQUFHLEVBQUUsR0FBRyxJQUFJLEdBQUcsSUFBSSxDQUFDO0FBRWxFLFNBQVMsZUFBZSxDQUN0QixLQUErQixFQUMvQixLQUF5QjtJQUV6QixJQUFJLEtBQUssS0FBSyxTQUFTO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFFMUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQzNDLE1BQU0sSUFBSSxLQUFLLENBQUMsdUJBQXVCLEtBQUssS0FBSyxLQUFLLG1DQUFtQyxDQUFDLENBQUM7SUFDN0YsQ0FBQztJQUVELE9BQU8sS0FBSyxDQUFDO0FBQ2YsQ0FBQztBQUVELFNBQWdCLGtCQUFrQixDQUNoQyxPQUF1QyxFQUN2QyxVQUFrQixFQUNsQixNQUFlO0lBRWYsSUFBSSxDQUFDLE9BQU87UUFBRSxPQUFPLFNBQVMsQ0FBQztJQUUvQixJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU87UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLGtDQUFrQyxDQUFDLENBQUM7SUFFMUUsTUFBTSxtQkFBbUIsR0FDdkIsZUFBZSxDQUFDLHFCQUFxQixFQUFFLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQztRQUNuRSw4QkFBOEIsQ0FBQztJQUNqQyxNQUFNLGdCQUFnQixHQUNwQixlQUFlLENBQUMsa0JBQWtCLEVBQUUsT0FBTyxDQUFDLGdCQUFnQixDQUFDLElBQUksMEJBQTBCLENBQUM7SUFFOUYsTUFBTSxRQUFRLEdBQUcsZUFBZSxDQUFDLFVBQVUsRUFBRSxPQUFPLENBQUMsUUFBUSxDQUFDLElBQUksaUJBQWlCLENBQUM7SUFDcEYsTUFBTSxzQkFBc0IsR0FDMUIsZUFBZSxDQUFDLHdCQUF3QixFQUFFLE9BQU8sQ0FBQyxzQkFBc0IsQ0FBQztRQUN6RSx5Q0FBaUMsQ0FBQztJQUVwQywwRkFBMEY7SUFDMUYsbUNBQW1DO0lBQ25DLElBQUksZ0JBQWdCLElBQUksbUJBQW1CLEVBQUUsQ0FBQztRQUM1QyxNQUFNLEVBQUUsQ0FDTixNQUFNLEVBQ04sZ0NBQWdDLGdCQUFnQixtQkFBbUI7WUFDakUsbUNBQW1DLG1CQUFtQixtQ0FBbUM7WUFDekYsNEJBQTRCLENBQy9CLENBQUM7SUFDSixDQUFDO0lBRUQsT0FBTztRQUNMLE9BQU8sRUFBRSxPQUFPLENBQUMsT0FBTztRQUN4QixTQUFTLEVBQUUsT0FBTyxDQUFDLFNBQVMsSUFBSSxrQkFBa0I7UUFDbEQsbUJBQW1CO1FBQ25CLGdCQUFnQjtRQUNoQixRQUFRO1FBQ1Isc0JBQXNCLEVBQ3BCLGVBQWUsQ0FBQyx3QkFBd0IsRUFBRSxPQUFPLENBQUMsc0JBQXNCLENBQUM7WUFDekUsZ0NBQWdDO1FBQ2xDLHNCQUFzQjtRQUN0QixVQUFVO1FBQ1YsYUFBYSxFQUFFLElBQUEsbUJBQWUsRUFDNUIsZUFBZSxDQUFDLHdCQUF3QixFQUFFLE9BQU8sQ0FBQyxzQkFBc0IsQ0FBQztZQUN2RSxnQ0FBZ0MsQ0FDbkM7S0FDRixDQUFDO0FBQ0osQ0FBQyJ9
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as ForestMCPServer } from './server';
|
|
2
2
|
export type { ForestMCPServerOptions, HttpCallback, ToolName } from './server';
|
|
3
3
|
export type { TokenTtlOptions } from './utils/token-ttl';
|
|
4
|
+
export type { FileUploadsOptions, UploadStorage } from './file-uploads/types';
|
|
4
5
|
export type { InProcessAgentDispatcher, InProcessDispatchRequest, InProcessDispatchResponse, } from './in-process-agent-dispatcher';
|
|
5
6
|
export { MCP_PATHS, isMcpRoute, makeIsMcpRoute } from './mcp-paths';
|
|
6
7
|
export { ForestServerClientImpl, createForestServerClient } from './http-client';
|
package/dist/index.js
CHANGED
|
@@ -14,4 +14,4 @@ Object.defineProperty(exports, "makeIsMcpRoute", { enumerable: true, get: functi
|
|
|
14
14
|
var http_client_1 = require("./http-client");
|
|
15
15
|
Object.defineProperty(exports, "ForestServerClientImpl", { enumerable: true, get: function () { return http_client_1.ForestServerClientImpl; } });
|
|
16
16
|
Object.defineProperty(exports, "createForestServerClient", { enumerable: true, get: function () { return http_client_1.createForestServerClient; } });
|
|
17
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
17
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEseUNBQXlDO0FBQ3pDLG1DQUFzRDtBQUE3QywwSEFBQSxPQUFPLE9BQW1CO0FBU25DLHlDQUFvRTtBQUEzRCxzR0FBQSxTQUFTLE9BQUE7QUFBRSx1R0FBQSxVQUFVLE9BQUE7QUFBRSwyR0FBQSxjQUFjLE9BQUE7QUFDOUMsNkNBQWlGO0FBQXhFLHFIQUFBLHNCQUFzQixPQUFBO0FBQUUsdUhBQUEsd0JBQXdCLE9BQUEifQ==
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import './polyfills';
|
|
2
|
+
import type { FileUploadsOptions } from './file-uploads/types';
|
|
2
3
|
import type { ForestServerClient } from './http-client';
|
|
3
4
|
import type { InProcessAgentDispatcher } from './in-process-agent-dispatcher';
|
|
4
5
|
import type { TokenTtlOptions } from './utils/token-ttl';
|
|
@@ -8,7 +9,7 @@ export declare const LOGO_URL = "https://forest-assets.s3.us-east-1.amazonaws.co
|
|
|
8
9
|
export type LogLevel = 'Debug' | 'Info' | 'Warn' | 'Error';
|
|
9
10
|
export type Logger = (level: LogLevel, message: string) => void;
|
|
10
11
|
export type HttpCallback = (req: http.IncomingMessage, res: http.ServerResponse, next?: () => void) => void;
|
|
11
|
-
export type ToolName = 'describeCollection' | 'list' | 'listRelated' | 'create' | 'update' | 'delete' | 'associate' | 'dissociate' | 'getActionForm' | 'executeAction';
|
|
12
|
+
export type ToolName = 'describeCollection' | 'list' | 'listRelated' | 'create' | 'update' | 'delete' | 'associate' | 'dissociate' | 'getActionForm' | 'executeAction' | 'requestActionFileUpload';
|
|
12
13
|
/**
|
|
13
14
|
* Options for configuring the Forest Admin MCP Server
|
|
14
15
|
*/
|
|
@@ -57,6 +58,16 @@ export interface ForestMCPServerOptions {
|
|
|
57
58
|
* Omit to accept any dynamically registered client.
|
|
58
59
|
*/
|
|
59
60
|
allowedOAuthClients?: string[];
|
|
61
|
+
/**
|
|
62
|
+
* Action file uploads are on by default, with the objects held in memory. Pass an object to
|
|
63
|
+
* configure them — a `storage` backend, size limits, ttls — or `false` to turn the feature off:
|
|
64
|
+
* no `requestActionFileUpload` tool, no upload endpoint, and `executeAction` stops mentioning
|
|
65
|
+
* either. See the README for the flow and the storage contract.
|
|
66
|
+
*
|
|
67
|
+
* @experimental Expected to change to follow the MCP file transfer specification once it
|
|
68
|
+
* lands (SEP-2631).
|
|
69
|
+
*/
|
|
70
|
+
fileUploads?: false | FileUploadsOptions;
|
|
60
71
|
}
|
|
61
72
|
/**
|
|
62
73
|
* Forest Admin MCP Server
|
|
@@ -80,6 +91,9 @@ export default class ForestMCPServer {
|
|
|
80
91
|
private agentDispatcher?;
|
|
81
92
|
private tokenTtl?;
|
|
82
93
|
private allowedOAuthClients?;
|
|
94
|
+
private fileUploadsOptions?;
|
|
95
|
+
private fileUploads?;
|
|
96
|
+
private ephemeralStorage?;
|
|
83
97
|
constructor(options?: ForestMCPServerOptions);
|
|
84
98
|
private createDefaultForestServerClient;
|
|
85
99
|
private fetchCollectionNames;
|