@opendatalabs/personal-server-ts-core 0.2.0 → 1.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/dist/api/index.d.ts +160 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +774 -0
- package/dist/api/index.js.map +1 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/request.d.ts +28 -0
- package/dist/auth/request.d.ts.map +1 -0
- package/dist/auth/request.js +106 -0
- package/dist/auth/request.js.map +1 -0
- package/dist/client.d.ts +233 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +243 -0
- package/dist/client.js.map +1 -0
- package/dist/config/defaults.d.ts +5 -5
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +3 -5
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +0 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +0 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +1 -9
- package/dist/config/paths.js.map +1 -1
- package/dist/contracts/access-logs.d.ts +8 -0
- package/dist/contracts/access-logs.d.ts.map +1 -0
- package/dist/contracts/access-logs.js +7 -0
- package/dist/contracts/access-logs.js.map +1 -0
- package/dist/contracts/auth-device.d.ts +70 -0
- package/dist/contracts/auth-device.d.ts.map +1 -0
- package/dist/contracts/auth-device.js +163 -0
- package/dist/contracts/auth-device.js.map +1 -0
- package/dist/contracts/binary.d.ts +54 -0
- package/dist/contracts/binary.d.ts.map +1 -0
- package/dist/contracts/binary.js +92 -0
- package/dist/contracts/binary.js.map +1 -0
- package/dist/contracts/config.d.ts +5 -0
- package/dist/contracts/config.d.ts.map +1 -0
- package/dist/contracts/config.js +30 -0
- package/dist/contracts/config.js.map +1 -0
- package/dist/contracts/data.d.ts +115 -0
- package/dist/contracts/data.d.ts.map +1 -0
- package/dist/contracts/data.js +237 -0
- package/dist/contracts/data.js.map +1 -0
- package/dist/contracts/grants.d.ts +45 -0
- package/dist/contracts/grants.d.ts.map +1 -0
- package/dist/contracts/grants.js +177 -0
- package/dist/contracts/grants.js.map +1 -0
- package/dist/contracts/http.d.ts +21 -0
- package/dist/contracts/http.d.ts.map +1 -0
- package/dist/contracts/http.js +49 -0
- package/dist/contracts/http.js.map +1 -0
- package/dist/contracts/index.d.ts +16 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +10 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/oauth.d.ts +38 -0
- package/dist/contracts/oauth.d.ts.map +1 -0
- package/dist/contracts/oauth.js +130 -0
- package/dist/contracts/oauth.js.map +1 -0
- package/dist/contracts/sync.d.ts +10 -0
- package/dist/contracts/sync.d.ts.map +1 -0
- package/dist/contracts/sync.js +34 -0
- package/dist/contracts/sync.js.map +1 -0
- package/dist/errors/catalog.d.ts +9 -0
- package/dist/errors/catalog.d.ts.map +1 -1
- package/dist/errors/catalog.js +15 -0
- package/dist/errors/catalog.js.map +1 -1
- package/dist/grants/index.d.ts +2 -4
- package/dist/grants/index.d.ts.map +1 -1
- package/dist/grants/index.js +1 -2
- package/dist/grants/index.js.map +1 -1
- package/dist/keys/index.d.ts +1 -2
- package/dist/keys/index.d.ts.map +1 -1
- package/dist/keys/index.js +1 -2
- package/dist/keys/index.js.map +1 -1
- package/dist/keys/server-account.d.ts +5 -9
- package/dist/keys/server-account.d.ts.map +1 -1
- package/dist/keys/server-account.js +1 -47
- package/dist/keys/server-account.js.map +1 -1
- package/dist/logger/index.d.ts +6 -2
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +2 -6
- package/dist/logger/index.js.map +1 -1
- package/dist/logging/access-log.d.ts +0 -1
- package/dist/logging/access-log.d.ts.map +1 -1
- package/dist/logging/access-log.js +1 -24
- package/dist/logging/access-log.js.map +1 -1
- package/dist/logging/access-reader.d.ts +0 -5
- package/dist/logging/access-reader.d.ts.map +1 -1
- package/dist/logging/access-reader.js +1 -49
- package/dist/logging/access-reader.js.map +1 -1
- package/dist/mcp/activity.d.ts +68 -0
- package/dist/mcp/activity.d.ts.map +1 -0
- package/dist/mcp/activity.js +111 -0
- package/dist/mcp/activity.js.map +1 -0
- package/dist/mcp/builder-registration.d.ts +24 -0
- package/dist/mcp/builder-registration.d.ts.map +1 -0
- package/dist/mcp/builder-registration.js +89 -0
- package/dist/mcp/builder-registration.js.map +1 -0
- package/dist/mcp/connection-api.d.ts +170 -0
- package/dist/mcp/connection-api.d.ts.map +1 -0
- package/dist/mcp/connection-api.js +379 -0
- package/dist/mcp/connection-api.js.map +1 -0
- package/dist/mcp/grantee.d.ts +52 -0
- package/dist/mcp/grantee.d.ts.map +1 -0
- package/dist/mcp/grantee.js +91 -0
- package/dist/mcp/grantee.js.map +1 -0
- package/dist/mcp/index.d.ts +10 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +10 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/read-client.d.ts +136 -0
- package/dist/mcp/read-client.d.ts.map +1 -0
- package/dist/mcp/read-client.js +306 -0
- package/dist/mcp/read-client.js.map +1 -0
- package/dist/mcp/resources.d.ts +37 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +114 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/search/index.d.ts +34 -0
- package/dist/mcp/search/index.d.ts.map +1 -0
- package/dist/mcp/search/index.js +69 -0
- package/dist/mcp/search/index.js.map +1 -0
- package/dist/mcp/server.d.ts +53 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +308 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/store.d.ts +13 -0
- package/dist/mcp/store.d.ts.map +1 -0
- package/dist/mcp/store.js +98 -0
- package/dist/mcp/store.js.map +1 -0
- package/dist/mcp/tools.d.ts +60 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +1058 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +97 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +24 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/payment/index.d.ts +2 -0
- package/dist/payment/index.d.ts.map +1 -0
- package/dist/payment/index.js +2 -0
- package/dist/payment/index.js.map +1 -0
- package/dist/payment/x402.d.ts +155 -0
- package/dist/payment/x402.d.ts.map +1 -0
- package/dist/payment/x402.js +402 -0
- package/dist/payment/x402.js.map +1 -0
- package/dist/policy/data-read.d.ts +15 -0
- package/dist/policy/data-read.d.ts.map +1 -0
- package/dist/policy/data-read.js +84 -0
- package/dist/policy/data-read.js.map +1 -0
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +2 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/ports/index.d.ts +95 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +7 -0
- package/dist/ports/index.js.map +1 -0
- package/dist/schemas/server-config.d.ts +12 -0
- package/dist/schemas/server-config.d.ts.map +1 -1
- package/dist/schemas/server-config.js +34 -4
- package/dist/schemas/server-config.js.map +1 -1
- package/dist/signing/index.d.ts +0 -1
- package/dist/signing/index.d.ts.map +1 -1
- package/dist/signing/index.js +0 -1
- package/dist/signing/index.js.map +1 -1
- package/dist/signing/request-signer.d.ts +8 -2
- package/dist/signing/request-signer.d.ts.map +1 -1
- package/dist/signing/request-signer.js +5 -31
- package/dist/signing/request-signer.js.map +1 -1
- package/dist/signing/signer.d.ts +20 -2
- package/dist/signing/signer.d.ts.map +1 -1
- package/dist/signing/signer.js +23 -13
- package/dist/signing/signer.js.map +1 -1
- package/dist/storage/adapters/index.d.ts +2 -1
- package/dist/storage/adapters/index.d.ts.map +1 -1
- package/dist/storage/adapters/index.js +2 -1
- package/dist/storage/adapters/index.js.map +1 -1
- package/dist/storage/adapters/interface.d.ts +10 -0
- package/dist/storage/adapters/interface.d.ts.map +1 -1
- package/dist/storage/adapters/sdk.d.ts +23 -0
- package/dist/storage/adapters/sdk.d.ts.map +1 -0
- package/dist/storage/adapters/sdk.js +73 -0
- package/dist/storage/adapters/sdk.js.map +1 -0
- package/dist/storage/adapters/vana.d.ts +7 -30
- package/dist/storage/adapters/vana.d.ts.map +1 -1
- package/dist/storage/adapters/vana.js +37 -92
- package/dist/storage/adapters/vana.js.map +1 -1
- package/dist/storage/blocks/build.d.ts +20 -0
- package/dist/storage/blocks/build.d.ts.map +1 -0
- package/dist/storage/blocks/build.js +378 -0
- package/dist/storage/blocks/build.js.map +1 -0
- package/dist/storage/blocks/cursor.d.ts +33 -0
- package/dist/storage/blocks/cursor.d.ts.map +1 -0
- package/dist/storage/blocks/cursor.js +142 -0
- package/dist/storage/blocks/cursor.js.map +1 -0
- package/dist/storage/blocks/errors.d.ts +6 -0
- package/dist/storage/blocks/errors.d.ts.map +1 -0
- package/dist/storage/blocks/errors.js +9 -0
- package/dist/storage/blocks/errors.js.map +1 -0
- package/dist/storage/blocks/index.d.ts +5 -0
- package/dist/storage/blocks/index.d.ts.map +1 -0
- package/dist/storage/blocks/index.js +4 -0
- package/dist/storage/blocks/index.js.map +1 -0
- package/dist/storage/blocks/types.d.ts +49 -0
- package/dist/storage/blocks/types.d.ts.map +1 -0
- package/dist/storage/blocks/types.js +2 -0
- package/dist/{grants → storage/blocks}/types.js.map +1 -1
- package/dist/storage/hierarchy/index.d.ts +0 -1
- package/dist/storage/hierarchy/index.d.ts.map +1 -1
- package/dist/storage/hierarchy/index.js +0 -1
- package/dist/storage/hierarchy/index.js.map +1 -1
- package/dist/storage/hierarchy/manager.d.ts +0 -14
- package/dist/storage/hierarchy/manager.d.ts.map +1 -1
- package/dist/storage/hierarchy/manager.js +1 -59
- package/dist/storage/hierarchy/manager.js.map +1 -1
- package/dist/storage/hierarchy/paths.d.ts.map +1 -1
- package/dist/storage/hierarchy/paths.js +11 -4
- package/dist/storage/hierarchy/paths.js.map +1 -1
- package/dist/storage/index/index.d.ts +2 -3
- package/dist/storage/index/index.d.ts.map +1 -1
- package/dist/storage/index/index.js +1 -2
- package/dist/storage/index/index.js.map +1 -1
- package/dist/storage/index/manager.d.ts +26 -6
- package/dist/storage/index/manager.d.ts.map +1 -1
- package/dist/storage/index/manager.js +1 -131
- package/dist/storage/index/manager.js.map +1 -1
- package/dist/storage/index/schema.d.ts +1 -3
- package/dist/storage/index/schema.d.ts.map +1 -1
- package/dist/storage/index/schema.js +1 -26
- package/dist/storage/index/schema.js.map +1 -1
- package/dist/storage/index/types.d.ts +16 -0
- package/dist/storage/index/types.d.ts.map +1 -1
- package/dist/storage/preview.d.ts +12 -0
- package/dist/storage/preview.d.ts.map +1 -0
- package/dist/storage/preview.js +239 -0
- package/dist/storage/preview.js.map +1 -0
- package/dist/sync/cursor.d.ts +5 -7
- package/dist/sync/cursor.d.ts.map +1 -1
- package/dist/sync/cursor.js +1 -18
- package/dist/sync/cursor.js.map +1 -1
- package/dist/sync/engine/sync-manager.d.ts +17 -0
- package/dist/sync/engine/sync-manager.d.ts.map +1 -1
- package/dist/sync/engine/sync-manager.js +104 -43
- package/dist/sync/engine/sync-manager.js.map +1 -1
- package/dist/sync/index.d.ts +4 -2
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/issues.d.ts +55 -0
- package/dist/sync/issues.d.ts.map +1 -0
- package/dist/sync/issues.js +168 -0
- package/dist/sync/issues.js.map +1 -0
- package/dist/sync/retry-memory.d.ts +33 -0
- package/dist/sync/retry-memory.d.ts.map +1 -0
- package/dist/sync/retry-memory.js +68 -0
- package/dist/sync/retry-memory.js.map +1 -0
- package/dist/sync/types.d.ts +9 -24
- package/dist/sync/types.d.ts.map +1 -1
- package/dist/sync/workers/delete.d.ts +42 -0
- package/dist/sync/workers/delete.d.ts.map +1 -0
- package/dist/sync/workers/delete.js +40 -0
- package/dist/sync/workers/delete.js.map +1 -0
- package/dist/sync/workers/download.d.ts +64 -18
- package/dist/sync/workers/download.d.ts.map +1 -1
- package/dist/sync/workers/download.js +396 -54
- package/dist/sync/workers/download.js.map +1 -1
- package/dist/sync/workers/upload.d.ts +29 -18
- package/dist/sync/workers/upload.d.ts.map +1 -1
- package/dist/sync/workers/upload.js +163 -46
- package/dist/sync/workers/upload.js.map +1 -1
- package/dist/test-utils/wallet.d.ts +1 -0
- package/dist/test-utils/wallet.d.ts.map +1 -1
- package/dist/test-utils/wallet.js +9 -31
- package/dist/test-utils/wallet.js.map +1 -1
- package/package.json +51 -27
- package/dist/auth/web3-signed.d.ts +0 -48
- package/dist/auth/web3-signed.d.ts.map +0 -1
- package/dist/auth/web3-signed.js +0 -110
- package/dist/auth/web3-signed.js.map +0 -1
- package/dist/config/loader.d.ts +0 -8
- package/dist/config/loader.d.ts.map +0 -1
- package/dist/config/loader.js +0 -38
- package/dist/config/loader.js.map +0 -1
- package/dist/gateway/client.d.ts +0 -98
- package/dist/gateway/client.d.ts.map +0 -1
- package/dist/gateway/client.js +0 -171
- package/dist/gateway/client.js.map +0 -1
- package/dist/grants/eip712.d.ts +0 -28
- package/dist/grants/eip712.d.ts.map +0 -1
- package/dist/grants/eip712.js +0 -30
- package/dist/grants/eip712.js.map +0 -1
- package/dist/grants/types.d.ts +0 -29
- package/dist/grants/types.d.ts.map +0 -1
- package/dist/grants/types.js +0 -6
- package/dist/grants/verify.d.ts +0 -24
- package/dist/grants/verify.d.ts.map +0 -1
- package/dist/grants/verify.js +0 -71
- package/dist/grants/verify.js.map +0 -1
- package/dist/keys/derive.d.ts +0 -18
- package/dist/keys/derive.d.ts.map +0 -1
- package/dist/keys/derive.js +0 -43
- package/dist/keys/derive.js.map +0 -1
- package/dist/schemas/data-file.d.ts +0 -20
- package/dist/schemas/data-file.d.ts.map +0 -1
- package/dist/schemas/data-file.js +0 -23
- package/dist/schemas/data-file.js.map +0 -1
- package/dist/scopes/index.d.ts +0 -3
- package/dist/scopes/index.d.ts.map +0 -1
- package/dist/scopes/index.js +0 -3
- package/dist/scopes/index.js.map +0 -1
- package/dist/scopes/match.d.ts +0 -13
- package/dist/scopes/match.d.ts.map +0 -1
- package/dist/scopes/match.js +0 -23
- package/dist/scopes/match.js.map +0 -1
- package/dist/scopes/parse.d.ts +0 -12
- package/dist/scopes/parse.d.ts.map +0 -1
- package/dist/scopes/parse.js +0 -29
- package/dist/scopes/parse.js.map +0 -1
- package/dist/signing/eip712.d.ts +0 -69
- package/dist/signing/eip712.d.ts.map +0 -1
- package/dist/signing/eip712.js +0 -68
- package/dist/signing/eip712.js.map +0 -1
- package/dist/storage/encryption/decrypt.d.ts +0 -10
- package/dist/storage/encryption/decrypt.d.ts.map +0 -1
- package/dist/storage/encryption/decrypt.js +0 -19
- package/dist/storage/encryption/decrypt.js.map +0 -1
- package/dist/storage/encryption/encrypt.d.ts +0 -10
- package/dist/storage/encryption/encrypt.d.ts.map +0 -1
- package/dist/storage/encryption/encrypt.js +0 -19
- package/dist/storage/encryption/encrypt.js.map +0 -1
- package/dist/storage/encryption/index.d.ts +0 -3
- package/dist/storage/encryption/index.d.ts.map +0 -1
- package/dist/storage/encryption/index.js +0 -3
- package/dist/storage/encryption/index.js.map +0 -1
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool surface. Phase 1 / §4 of 260604-PLAN-vana-mcp-personal-server.md.
|
|
3
|
+
*
|
|
4
|
+
* Read-only, small, and only over scopes that the connection's grants cover.
|
|
5
|
+
* No write tools. No grant-approval tools. No raw owner/admin status tools.
|
|
6
|
+
*
|
|
7
|
+
* Every tool that returns data first picks a covering grant from the
|
|
8
|
+
* connection's grant list, signs a Web3Signed request as the per-connection
|
|
9
|
+
* grantee, and uses the bounded sidecar read path — so reads are grant-gated,
|
|
10
|
+
* access-logged, and safe for large scopes.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { McpDataReadError } from "./read-client.js";
|
|
14
|
+
import { MiniSearchIndex } from "./search/index.js";
|
|
15
|
+
import { rawScopeFileInputSchema, rawScopeMetadata, rawScopeResourceUri, } from "./resources.js";
|
|
16
|
+
const STRUCTURED_CONTENT_TEXT_LIMIT_BYTES = 64 * 1024;
|
|
17
|
+
const resultTextEncoder = new TextEncoder();
|
|
18
|
+
function textResult(value, isError = false) {
|
|
19
|
+
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
20
|
+
const textBytes = resultTextEncoder.encode(text).byteLength;
|
|
21
|
+
const structuredContent = !isError &&
|
|
22
|
+
textBytes <= STRUCTURED_CONTENT_TEXT_LIMIT_BYTES &&
|
|
23
|
+
typeof value === "object" &&
|
|
24
|
+
value !== null &&
|
|
25
|
+
!Array.isArray(value)
|
|
26
|
+
? value
|
|
27
|
+
: undefined;
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
text,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
...(structuredContent ? { structuredContent } : {}),
|
|
36
|
+
...(isError ? { isError: true } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function uniqueScopes(connection) {
|
|
40
|
+
const set = new Set();
|
|
41
|
+
for (const grant of connection.grants) {
|
|
42
|
+
for (const scope of grant.scopes)
|
|
43
|
+
set.add(scope);
|
|
44
|
+
}
|
|
45
|
+
return Array.from(set).sort();
|
|
46
|
+
}
|
|
47
|
+
function uniqueSources(connection) {
|
|
48
|
+
const set = new Set();
|
|
49
|
+
for (const grant of connection.grants) {
|
|
50
|
+
const sourceId = typeof grant.sourceId === "string" ? grant.sourceId.trim() : "";
|
|
51
|
+
if (sourceId) {
|
|
52
|
+
set.add(sourceId);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const scope of grant.scopes) {
|
|
56
|
+
const [scopeSourceId] = scope.split(".");
|
|
57
|
+
if (scopeSourceId && scopeSourceId !== "*") {
|
|
58
|
+
set.add(scopeSourceId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Array.from(set).sort();
|
|
63
|
+
}
|
|
64
|
+
function normalizeScopeListInput(value) {
|
|
65
|
+
if (!Array.isArray(value))
|
|
66
|
+
return [];
|
|
67
|
+
const scopes = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (const raw of value) {
|
|
70
|
+
if (typeof raw !== "string")
|
|
71
|
+
continue;
|
|
72
|
+
const scope = raw.trim();
|
|
73
|
+
if (!scope || scope.length > SEARCH_SCOPE_MAX_CHARS || seen.has(scope)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
seen.add(scope);
|
|
77
|
+
scopes.push(scope);
|
|
78
|
+
}
|
|
79
|
+
return scopes;
|
|
80
|
+
}
|
|
81
|
+
const DEFAULT_SEARCH_LIMIT = 5;
|
|
82
|
+
const MAX_SEARCH_LIMIT = 50;
|
|
83
|
+
const MAX_SEARCH_SCOPES = 200;
|
|
84
|
+
const DEFAULT_SEARCH_MAX_SCOPES = MAX_SEARCH_SCOPES;
|
|
85
|
+
const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
|
|
86
|
+
const MAX_SEARCH_TIMEOUT_MS = 300_000;
|
|
87
|
+
const DEFAULT_SEARCH_DISCOVERY_TIMEOUT_MS = 2_000;
|
|
88
|
+
const MAX_SEARCH_TOTAL_TIMEOUT_MS = 300_000;
|
|
89
|
+
const DEFAULT_READ_SCOPE_MAX_BYTES = 64 * 1024;
|
|
90
|
+
const MAX_READ_SCOPE_MAX_BYTES = 5 * 1024 * 1024;
|
|
91
|
+
const DEFAULT_READ_SCOPE_TIMEOUT_MS = 60_000;
|
|
92
|
+
const MAX_READ_SCOPE_TIMEOUT_MS = 300_000;
|
|
93
|
+
const DEFAULT_READ_FILE_TIMEOUT_MS = 60_000;
|
|
94
|
+
const MAX_READ_FILE_TIMEOUT_MS = 300_000;
|
|
95
|
+
const DEFAULT_READ_FILE_MAX_BYTES = 10 * 1024 * 1024;
|
|
96
|
+
const MAX_READ_FILE_MAX_BYTES = 25 * 1024 * 1024;
|
|
97
|
+
const DEFAULT_SEARCH_MAX_BYTES = 64 * 1024;
|
|
98
|
+
const MAX_SEARCH_MAX_BYTES = 1024 * 1024;
|
|
99
|
+
const DEFAULT_SCOPE_METADATA_TIMEOUT_MS = 2_000;
|
|
100
|
+
const SEARCH_MAX_PAGES_PER_SCOPE = 16;
|
|
101
|
+
const SEARCH_QUERY_MAX_CHARS = 256;
|
|
102
|
+
const SEARCH_SCOPE_MAX_CHARS = 128;
|
|
103
|
+
const SEARCH_REQUESTED_SCOPES_LIMIT = MAX_SEARCH_SCOPES * 2;
|
|
104
|
+
const SEARCH_SKIPPED_SCOPES_LIMIT = 50;
|
|
105
|
+
const SEARCH_PREVIEW_CHARS = 600;
|
|
106
|
+
const LIST_SCOPES_LIMIT = 200;
|
|
107
|
+
// Size thresholds for sizeClass classification (raw envelope bytes)
|
|
108
|
+
const SIZE_CLASS_TINY_BYTES = 10_000;
|
|
109
|
+
const SIZE_CLASS_SMALL_BYTES = 100_000;
|
|
110
|
+
const SIZE_CLASS_MEDIUM_BYTES = 1_000_000;
|
|
111
|
+
const SIZE_CLASS_LARGE_BYTES = 10_000_000;
|
|
112
|
+
class OperationTimeoutError extends Error {
|
|
113
|
+
operation;
|
|
114
|
+
timeoutMs;
|
|
115
|
+
constructor(operation, timeoutMs) {
|
|
116
|
+
super(`${operation} timed out after ${timeoutMs}ms`);
|
|
117
|
+
this.operation = operation;
|
|
118
|
+
this.timeoutMs = timeoutMs;
|
|
119
|
+
this.name = "OperationTimeoutError";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a concrete requested scope to a covering grant id from the
|
|
124
|
+
* connection's approved grants. A scope is "covered" when an approved grant's
|
|
125
|
+
* scopes either contains the exact scope or contains a wildcard like
|
|
126
|
+
* `instagram.*` that matches a concrete child scope like `instagram.profile`.
|
|
127
|
+
*
|
|
128
|
+
* Mirrors `scopeCoveredByGrant` semantics from `policy/data-read.ts` so the
|
|
129
|
+
* tool surface never offers Claude a scope its grant won't actually pass
|
|
130
|
+
* server-side.
|
|
131
|
+
*/
|
|
132
|
+
function resolveGrantForScope(connection, scope) {
|
|
133
|
+
for (const grant of connection.grants) {
|
|
134
|
+
for (const granted of grant.scopes) {
|
|
135
|
+
if (granted === scope) {
|
|
136
|
+
return { grantId: grant.grantId, scopes: grant.scopes };
|
|
137
|
+
}
|
|
138
|
+
if (granted.endsWith(".*")) {
|
|
139
|
+
const prefix = granted.slice(0, -2);
|
|
140
|
+
if (scope.startsWith(`${prefix}.`)) {
|
|
141
|
+
return { grantId: grant.grantId, scopes: grant.scopes };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (granted === "*") {
|
|
145
|
+
return { grantId: grant.grantId, scopes: grant.scopes };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function clampInteger(value, fallback, min, max) {
|
|
152
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
153
|
+
return fallback;
|
|
154
|
+
}
|
|
155
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
156
|
+
}
|
|
157
|
+
function wildcardPrefix(scope) {
|
|
158
|
+
if (scope === "*")
|
|
159
|
+
return "";
|
|
160
|
+
if (scope.endsWith(".*"))
|
|
161
|
+
return `${scope.slice(0, -2)}.`;
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
function truncateLabel(value) {
|
|
165
|
+
return value.length > SEARCH_SCOPE_MAX_CHARS
|
|
166
|
+
? `${value.slice(0, SEARCH_SCOPE_MAX_CHARS)}...`
|
|
167
|
+
: value;
|
|
168
|
+
}
|
|
169
|
+
function stringifyPreview(value) {
|
|
170
|
+
if (typeof value === "string") {
|
|
171
|
+
return value.slice(0, SEARCH_PREVIEW_CHARS);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
return JSON.stringify(value)?.slice(0, SEARCH_PREVIEW_CHARS) ?? "";
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return String(value).slice(0, SEARCH_PREVIEW_CHARS);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function collectSearchText(value) {
|
|
181
|
+
const chunks = [];
|
|
182
|
+
function visit(node) {
|
|
183
|
+
if (typeof node === "string") {
|
|
184
|
+
chunks.push(node);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (typeof node === "number" ||
|
|
188
|
+
typeof node === "boolean" ||
|
|
189
|
+
node === null) {
|
|
190
|
+
chunks.push(String(node));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(node)) {
|
|
194
|
+
for (const item of node)
|
|
195
|
+
visit(item);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (typeof node === "object" && node !== null) {
|
|
199
|
+
for (const [key, item] of Object.entries(node)) {
|
|
200
|
+
chunks.push(key);
|
|
201
|
+
visit(item);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
visit(value);
|
|
206
|
+
return chunks.join("\n");
|
|
207
|
+
}
|
|
208
|
+
function blockPageSearchText(blocks) {
|
|
209
|
+
return blocks
|
|
210
|
+
.map((block) => `${block.path}\n${collectSearchText(block.value)}`)
|
|
211
|
+
.join("\n\n");
|
|
212
|
+
}
|
|
213
|
+
function blockPageSearchDocuments(scope, blocks) {
|
|
214
|
+
const textById = new Map();
|
|
215
|
+
const documents = blocks.map((block, index) => {
|
|
216
|
+
const id = `${scope}:${block.id || index}`;
|
|
217
|
+
const text = `${block.path}\n${collectSearchText(block.value)}`;
|
|
218
|
+
textById.set(id, text);
|
|
219
|
+
return {
|
|
220
|
+
id,
|
|
221
|
+
scope,
|
|
222
|
+
title: block.path,
|
|
223
|
+
text,
|
|
224
|
+
preview: text.slice(0, SEARCH_PREVIEW_CHARS),
|
|
225
|
+
blockRef: block.id,
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
return { documents, textById };
|
|
229
|
+
}
|
|
230
|
+
function previewMatch(text, matchIndex) {
|
|
231
|
+
const halfWindow = Math.floor(SEARCH_PREVIEW_CHARS / 2);
|
|
232
|
+
const start = Math.max(0, matchIndex - halfWindow);
|
|
233
|
+
const end = Math.min(text.length, start + SEARCH_PREVIEW_CHARS);
|
|
234
|
+
const prefix = start > 0 ? "..." : "";
|
|
235
|
+
const suffix = end < text.length ? "..." : "";
|
|
236
|
+
return `${prefix}${text.slice(start, end)}${suffix}`;
|
|
237
|
+
}
|
|
238
|
+
function miniSearchMatchIndex(text, terms) {
|
|
239
|
+
const normalized = text.toLowerCase();
|
|
240
|
+
for (const term of terms) {
|
|
241
|
+
const index = normalized.indexOf(term.toLowerCase());
|
|
242
|
+
if (index >= 0)
|
|
243
|
+
return index;
|
|
244
|
+
}
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
async function withTimeout(promise, timeoutMs, operation) {
|
|
248
|
+
let timeout;
|
|
249
|
+
try {
|
|
250
|
+
return await Promise.race([
|
|
251
|
+
promise,
|
|
252
|
+
new Promise((_resolve, reject) => {
|
|
253
|
+
timeout = setTimeout(() => reject(new OperationTimeoutError(operation, timeoutMs)), timeoutMs);
|
|
254
|
+
}),
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
if (timeout)
|
|
259
|
+
clearTimeout(timeout);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function normalizeRequestedScopes(value, connection) {
|
|
263
|
+
const input = Array.isArray(value) ? value : uniqueScopes(connection);
|
|
264
|
+
const requestedScopes = [];
|
|
265
|
+
const seen = new Set();
|
|
266
|
+
const skippedScopes = [];
|
|
267
|
+
const scanned = Math.min(input.length, SEARCH_REQUESTED_SCOPES_LIMIT);
|
|
268
|
+
let invalidCount = 0;
|
|
269
|
+
for (let index = 0; index < scanned; index += 1) {
|
|
270
|
+
const scope = input[index];
|
|
271
|
+
if (typeof scope !== "string") {
|
|
272
|
+
invalidCount += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const trimmed = scope.trim();
|
|
276
|
+
if (!trimmed || trimmed.length > SEARCH_SCOPE_MAX_CHARS) {
|
|
277
|
+
invalidCount += 1;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (seen.has(trimmed))
|
|
281
|
+
continue;
|
|
282
|
+
seen.add(trimmed);
|
|
283
|
+
requestedScopes.push(trimmed);
|
|
284
|
+
}
|
|
285
|
+
if (input.length > SEARCH_REQUESTED_SCOPES_LIMIT) {
|
|
286
|
+
skippedScopes.push(`${input.length - SEARCH_REQUESTED_SCOPES_LIMIT} requested scopes omitted by input cap`);
|
|
287
|
+
}
|
|
288
|
+
if (invalidCount > 0) {
|
|
289
|
+
skippedScopes.push(`${invalidCount} invalid requested scopes omitted`);
|
|
290
|
+
}
|
|
291
|
+
return { requestedScopes, skippedScopes };
|
|
292
|
+
}
|
|
293
|
+
function classifySizeBytes(sizeBytes) {
|
|
294
|
+
if (sizeBytes === undefined)
|
|
295
|
+
return "unknown";
|
|
296
|
+
if (sizeBytes < SIZE_CLASS_TINY_BYTES)
|
|
297
|
+
return "tiny";
|
|
298
|
+
if (sizeBytes < SIZE_CLASS_SMALL_BYTES)
|
|
299
|
+
return "small";
|
|
300
|
+
if (sizeBytes < SIZE_CLASS_MEDIUM_BYTES)
|
|
301
|
+
return "medium";
|
|
302
|
+
if (sizeBytes < SIZE_CLASS_LARGE_BYTES)
|
|
303
|
+
return "large";
|
|
304
|
+
return "huge";
|
|
305
|
+
}
|
|
306
|
+
// Directive guidance the MCP client (e.g. Claude) uses to pick read_scope vs
|
|
307
|
+
// search_personal_context for a scope WITHOUT operator intervention. The signal
|
|
308
|
+
// must be unambiguous: a misnamed flag here (the old `searchRecommended: false`
|
|
309
|
+
// on large scopes) reads to a model as "don't search → read the whole thing",
|
|
310
|
+
// which is exactly the slow path — a full read of a large scope returns many
|
|
311
|
+
// pages it must walk via nextCursor and tends to stall the agent loop.
|
|
312
|
+
function recommendAccessForScope(sizeClass, hasBlocks) {
|
|
313
|
+
if (!hasBlocks) {
|
|
314
|
+
return {
|
|
315
|
+
recommendedAccess: "wait",
|
|
316
|
+
reason: "bounded block data is still indexing or unavailable; retry shortly before reading or searching",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (sizeClass === "large" || sizeClass === "huge") {
|
|
320
|
+
return {
|
|
321
|
+
recommendedAccess: "search",
|
|
322
|
+
reason: `scope is ${sizeClass}; prefer search_personal_context and name this scope explicitly. A full read_scope returns many pages you must walk via nextCursor.`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (sizeClass === "unknown") {
|
|
326
|
+
return {
|
|
327
|
+
recommendedAccess: "search",
|
|
328
|
+
reason: "scope size is unknown; prefer search_personal_context and name this scope explicitly",
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
recommendedAccess: "read",
|
|
333
|
+
reason: `scope is ${sizeClass}; read_scope returns it in one or few pages. search_personal_context also works for targeted lookups.`,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async function resolveSearchScopes({ connection, maxScopes, discoveryTimeoutMs, readClient, requestedScopes, }) {
|
|
337
|
+
const scopes = new Set();
|
|
338
|
+
const skippedScopes = [];
|
|
339
|
+
let skippedScopeOverflow = 0;
|
|
340
|
+
const errors = [];
|
|
341
|
+
let wildcardDiscoveries = 0;
|
|
342
|
+
function skipScope(scope) {
|
|
343
|
+
if (skippedScopes.length < SEARCH_SKIPPED_SCOPES_LIMIT) {
|
|
344
|
+
skippedScopes.push(truncateLabel(scope));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
skippedScopeOverflow += 1;
|
|
348
|
+
}
|
|
349
|
+
function addScope(scope) {
|
|
350
|
+
if (scopes.has(scope))
|
|
351
|
+
return;
|
|
352
|
+
if (scopes.size >= maxScopes) {
|
|
353
|
+
skipScope(scope);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
scopes.add(scope);
|
|
357
|
+
}
|
|
358
|
+
for (const requested of requestedScopes) {
|
|
359
|
+
if (scopes.size >= maxScopes) {
|
|
360
|
+
skipScope(requested);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const grant = resolveGrantForScope(connection, requested);
|
|
364
|
+
if (!grant) {
|
|
365
|
+
errors.push({ scope: requested, error: "scope_not_granted" });
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const prefix = wildcardPrefix(requested);
|
|
369
|
+
if (prefix === null) {
|
|
370
|
+
addScope(requested);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (wildcardDiscoveries >= maxScopes) {
|
|
374
|
+
skipScope(`${requested} (wildcard discovery cap reached)`);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
wildcardDiscoveries += 1;
|
|
378
|
+
try {
|
|
379
|
+
const listed = await withTimeout(readClient.listScopes({
|
|
380
|
+
scopePrefix: prefix,
|
|
381
|
+
limit: LIST_SCOPES_LIMIT,
|
|
382
|
+
}), discoveryTimeoutMs, `list scopes for ${requested}`);
|
|
383
|
+
for (const summary of listed.scopes) {
|
|
384
|
+
if (resolveGrantForScope(connection, summary.scope)) {
|
|
385
|
+
addScope(summary.scope);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (listed.total > listed.scopes.length) {
|
|
389
|
+
skipScope(`${requested} (more than ${listed.scopes.length})`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
errors.push({
|
|
394
|
+
scope: requested,
|
|
395
|
+
error: err instanceof OperationTimeoutError
|
|
396
|
+
? "scope_list_timeout"
|
|
397
|
+
: "scope_list_failed",
|
|
398
|
+
status: err instanceof McpDataReadError ? err.status : undefined,
|
|
399
|
+
bodyPreview: stringifyPreview(err instanceof McpDataReadError
|
|
400
|
+
? err.body
|
|
401
|
+
: err instanceof OperationTimeoutError
|
|
402
|
+
? err.message
|
|
403
|
+
: "unexpected scope list failure"),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (skippedScopeOverflow > 0) {
|
|
408
|
+
skippedScopes.push(`${skippedScopeOverflow} additional skipped scopes omitted`);
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
scopes: Array.from(scopes),
|
|
412
|
+
skippedScopes: Array.from(new Set(skippedScopes)),
|
|
413
|
+
errors,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const listGrantedSources = {
|
|
417
|
+
name: "list_granted_sources",
|
|
418
|
+
title: "List granted sources",
|
|
419
|
+
description: "List granted data source ids. Use list_granted_scopes for scope readiness and size.",
|
|
420
|
+
inputSchema: {},
|
|
421
|
+
async handler(_args, { connection }) {
|
|
422
|
+
return textResult({ sources: uniqueSources(connection) });
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
const listGrantedScopes = {
|
|
426
|
+
name: "list_granted_scopes",
|
|
427
|
+
title: "List granted scopes",
|
|
428
|
+
description: "List granted scopes, readiness, size, and recommendedAccess. Call first.",
|
|
429
|
+
inputSchema: {},
|
|
430
|
+
async handler(_args, { connection, readClient }) {
|
|
431
|
+
const grantedScopes = uniqueScopes(connection);
|
|
432
|
+
const scopeEntries = await Promise.all(grantedScopes.map(async (scope) => {
|
|
433
|
+
let meta = null;
|
|
434
|
+
if (typeof readClient.getScopeMetadata === "function") {
|
|
435
|
+
try {
|
|
436
|
+
meta = await withTimeout(readClient.getScopeMetadata(scope), DEFAULT_SCOPE_METADATA_TIMEOUT_MS, `scope metadata for ${scope}`);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return {
|
|
440
|
+
scope,
|
|
441
|
+
source: scope.split(".")[0],
|
|
442
|
+
dataStatus: "indexing",
|
|
443
|
+
sizeClass: "unknown",
|
|
444
|
+
recommendedAccess: "wait",
|
|
445
|
+
reason: "scope metadata is still indexing or temporarily unavailable; retry shortly",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (!meta) {
|
|
450
|
+
return {
|
|
451
|
+
scope,
|
|
452
|
+
dataStatus: "needs_refresh",
|
|
453
|
+
sizeClass: "unknown",
|
|
454
|
+
recommendedAccess: "wait",
|
|
455
|
+
reason: "no local data found; refresh your data connection",
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const sizeClass = classifySizeBytes(meta.sizeBytes);
|
|
459
|
+
const { recommendedAccess, reason } = recommendAccessForScope(sizeClass, meta.hasBlocks);
|
|
460
|
+
return {
|
|
461
|
+
scope,
|
|
462
|
+
source: scope.split(".")[0],
|
|
463
|
+
collectedAt: meta.collectedAt,
|
|
464
|
+
dataStatus: meta.hasBlocks
|
|
465
|
+
? "ready"
|
|
466
|
+
: "indexing",
|
|
467
|
+
sizeBytes: meta.sizeBytes,
|
|
468
|
+
sizeClass,
|
|
469
|
+
recommendedAccess,
|
|
470
|
+
reason,
|
|
471
|
+
};
|
|
472
|
+
}));
|
|
473
|
+
return textResult({ scopes: scopeEntries });
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
const requestScopeAccess = {
|
|
477
|
+
name: "request_scope_access",
|
|
478
|
+
title: "Request scope access",
|
|
479
|
+
description: "Check which requested scopes are missing from this MCP grant and tell the user to approve them in Vana.",
|
|
480
|
+
inputSchema: {
|
|
481
|
+
scopes: z
|
|
482
|
+
.array(z.string().min(1).max(SEARCH_SCOPE_MAX_CHARS))
|
|
483
|
+
.min(1)
|
|
484
|
+
.max(SEARCH_REQUESTED_SCOPES_LIMIT)
|
|
485
|
+
.describe("Exact scope ids the MCP client wants to read."),
|
|
486
|
+
reason: z
|
|
487
|
+
.string()
|
|
488
|
+
.max(500)
|
|
489
|
+
.optional()
|
|
490
|
+
.describe("Brief reason to show the user."),
|
|
491
|
+
},
|
|
492
|
+
async handler(args, { connection }) {
|
|
493
|
+
const requestedScopes = normalizeScopeListInput(args.scopes);
|
|
494
|
+
const grantedRequestedScopes = requestedScopes.filter((scope) => Boolean(resolveGrantForScope(connection, scope)));
|
|
495
|
+
const missingScopes = requestedScopes.filter((scope) => !resolveGrantForScope(connection, scope));
|
|
496
|
+
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
497
|
+
const approvalRequired = missingScopes.length > 0;
|
|
498
|
+
return textResult({
|
|
499
|
+
approvalRequired,
|
|
500
|
+
client: {
|
|
501
|
+
id: connection.id,
|
|
502
|
+
displayName: connection.displayName,
|
|
503
|
+
status: connection.status,
|
|
504
|
+
},
|
|
505
|
+
requestedScopes,
|
|
506
|
+
grantedRequestedScopes,
|
|
507
|
+
missingScopes,
|
|
508
|
+
grantedScopes: uniqueScopes(connection),
|
|
509
|
+
reason: reason || undefined,
|
|
510
|
+
nextAction: approvalRequired
|
|
511
|
+
? "Ask the user to open Vana's Personal Server page and add the missing scopes to this MCP client. This tool cannot grant access by itself."
|
|
512
|
+
: "No new grant is needed for the requested scopes.",
|
|
513
|
+
});
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
const readScope = {
|
|
517
|
+
name: "read_scope",
|
|
518
|
+
title: "Read scope",
|
|
519
|
+
description: "Read approved scope blocks. Best for small scopes. Use nextCursor for more pages; prefer search for large scopes.",
|
|
520
|
+
inputSchema: {
|
|
521
|
+
scope: z.string().min(1).describe("Exact scope id."),
|
|
522
|
+
cursor: z.string().min(1).optional(),
|
|
523
|
+
maxBytes: z.number().int().min(1).max(MAX_READ_SCOPE_MAX_BYTES).optional(),
|
|
524
|
+
timeoutMs: z
|
|
525
|
+
.number()
|
|
526
|
+
.int()
|
|
527
|
+
.min(1000)
|
|
528
|
+
.max(MAX_READ_SCOPE_TIMEOUT_MS)
|
|
529
|
+
.optional(),
|
|
530
|
+
},
|
|
531
|
+
async handler(args, { connection, readClient }) {
|
|
532
|
+
const scope = typeof args.scope === "string" ? args.scope : null;
|
|
533
|
+
if (!scope) {
|
|
534
|
+
return textResult({ error: "scope is required and must be a string" }, true);
|
|
535
|
+
}
|
|
536
|
+
const grant = resolveGrantForScope(connection, scope);
|
|
537
|
+
if (!grant) {
|
|
538
|
+
return textResult({
|
|
539
|
+
error: "scope_not_granted",
|
|
540
|
+
message: `Scope '${scope}' is not covered by any grant on this MCP connection.`,
|
|
541
|
+
grantedScopes: uniqueScopes(connection),
|
|
542
|
+
}, true);
|
|
543
|
+
}
|
|
544
|
+
const cursor = typeof args.cursor === "string" ? args.cursor : undefined;
|
|
545
|
+
const maxBytes = typeof args.maxBytes === "number"
|
|
546
|
+
? clampInteger(args.maxBytes, DEFAULT_READ_SCOPE_MAX_BYTES, 1, MAX_READ_SCOPE_MAX_BYTES)
|
|
547
|
+
: DEFAULT_READ_SCOPE_MAX_BYTES;
|
|
548
|
+
const timeoutMs = clampInteger(args.timeoutMs, DEFAULT_READ_SCOPE_TIMEOUT_MS, 1000, MAX_READ_SCOPE_TIMEOUT_MS);
|
|
549
|
+
// In-band steer: the model may call read_scope on a large scope without
|
|
550
|
+
// first consulting list_granted_scopes. Fetch lightweight planning metadata
|
|
551
|
+
// (storage-index only, no network/auth) opportunistically so we can attach a
|
|
552
|
+
// `guidance` hint when it is already available. This is advisory only: it
|
|
553
|
+
// must never delay the read or consume the read timeout budget.
|
|
554
|
+
let guidance;
|
|
555
|
+
if (typeof readClient.getScopeMetadata === "function") {
|
|
556
|
+
void withTimeout(readClient.getScopeMetadata(scope), DEFAULT_SCOPE_METADATA_TIMEOUT_MS, `scope metadata for ${scope}`)
|
|
557
|
+
.then((meta) => {
|
|
558
|
+
if (!meta)
|
|
559
|
+
return;
|
|
560
|
+
const sizeClass = classifySizeBytes(meta.sizeBytes);
|
|
561
|
+
const recommendation = recommendAccessForScope(sizeClass, meta.hasBlocks);
|
|
562
|
+
// Only steer when search is the better tool; a normal small read gets
|
|
563
|
+
// no extra noise.
|
|
564
|
+
if (recommendation.recommendedAccess === "search") {
|
|
565
|
+
guidance = {
|
|
566
|
+
sizeClass,
|
|
567
|
+
sizeBytes: meta.sizeBytes,
|
|
568
|
+
recommendedAccess: recommendation.recommendedAccess,
|
|
569
|
+
reason: recommendation.reason,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
.catch(() => {
|
|
574
|
+
// Advisory only — serve the read without guidance.
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
const result = await withTimeout(readClient.readScopeBlocks({
|
|
579
|
+
scope,
|
|
580
|
+
grantId: grant.grantId,
|
|
581
|
+
cursor,
|
|
582
|
+
maxBytes,
|
|
583
|
+
}), timeoutMs, `read blocks for ${scope}`);
|
|
584
|
+
const nextCursor = result.nextCursor ?? null;
|
|
585
|
+
return textResult({
|
|
586
|
+
scope,
|
|
587
|
+
grantId: grant.grantId,
|
|
588
|
+
collectedAt: result.collectedAt,
|
|
589
|
+
contentKind: result.contentKind,
|
|
590
|
+
blocks: result.blocks,
|
|
591
|
+
...(result.nextCursor ? { nextCursor: result.nextCursor } : {}),
|
|
592
|
+
...(guidance ? { guidance } : {}),
|
|
593
|
+
warnings: result.warnings,
|
|
594
|
+
page: {
|
|
595
|
+
cursor: cursor ?? null,
|
|
596
|
+
nextCursor,
|
|
597
|
+
hasMore: Boolean(result.nextCursor),
|
|
598
|
+
maxBytes,
|
|
599
|
+
timeoutMs,
|
|
600
|
+
returnedBlocks: result.blocks.length,
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
if (err instanceof OperationTimeoutError) {
|
|
606
|
+
return textResult({
|
|
607
|
+
error: "scope_read_timeout",
|
|
608
|
+
message: err.message,
|
|
609
|
+
timeoutMs: err.timeoutMs,
|
|
610
|
+
}, true);
|
|
611
|
+
}
|
|
612
|
+
if (err instanceof McpDataReadError) {
|
|
613
|
+
if (err.status === 503) {
|
|
614
|
+
return textResult({
|
|
615
|
+
error: "bounded_data_unavailable",
|
|
616
|
+
status: err.status,
|
|
617
|
+
body: err.body,
|
|
618
|
+
}, true);
|
|
619
|
+
}
|
|
620
|
+
return textResult({ error: "data_read_failed", status: err.status, body: err.body }, true);
|
|
621
|
+
}
|
|
622
|
+
return textResult({
|
|
623
|
+
error: "tool_handler_error",
|
|
624
|
+
message: err instanceof Error ? err.message : String(err),
|
|
625
|
+
}, true);
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
function encodeSearchCursor(payload) {
|
|
630
|
+
const json = JSON.stringify(payload);
|
|
631
|
+
// TextEncoder + Uint8Array → base64url without Node.js Buffer dependency
|
|
632
|
+
const bytes = new TextEncoder().encode(json);
|
|
633
|
+
let binary = "";
|
|
634
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
635
|
+
binary += String.fromCharCode(bytes[i]);
|
|
636
|
+
}
|
|
637
|
+
return btoa(binary)
|
|
638
|
+
.replace(/\+/g, "-")
|
|
639
|
+
.replace(/\//g, "_")
|
|
640
|
+
.replace(/=+$/, "");
|
|
641
|
+
}
|
|
642
|
+
function decodeSearchCursor(raw) {
|
|
643
|
+
try {
|
|
644
|
+
const base64 = raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
645
|
+
const binary = atob(base64);
|
|
646
|
+
const bytes = new Uint8Array(binary.length);
|
|
647
|
+
for (let i = 0; i < binary.length; i++) {
|
|
648
|
+
bytes[i] = binary.charCodeAt(i);
|
|
649
|
+
}
|
|
650
|
+
const json = new TextDecoder().decode(bytes);
|
|
651
|
+
const payload = JSON.parse(json);
|
|
652
|
+
if (typeof payload === "object" &&
|
|
653
|
+
payload !== null &&
|
|
654
|
+
typeof payload.scopeIndex === "number") {
|
|
655
|
+
return {
|
|
656
|
+
scopeIndex: payload.scopeIndex,
|
|
657
|
+
blockCursor: typeof payload.blockCursor === "string"
|
|
658
|
+
? payload.blockCursor
|
|
659
|
+
: undefined,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const searchPersonalContext = {
|
|
669
|
+
name: "search_personal_context",
|
|
670
|
+
title: "Search personal context",
|
|
671
|
+
description: "Search approved scopes. Omit scopes for default sweep; name scopes for targeted search. Continue with nextSearchCursor.",
|
|
672
|
+
inputSchema: {
|
|
673
|
+
query: z.string().min(1).max(SEARCH_QUERY_MAX_CHARS),
|
|
674
|
+
scopes: z
|
|
675
|
+
.array(z.string().min(1).max(SEARCH_SCOPE_MAX_CHARS))
|
|
676
|
+
.max(SEARCH_REQUESTED_SCOPES_LIMIT)
|
|
677
|
+
.optional(),
|
|
678
|
+
cursor: z.string().min(1).optional(),
|
|
679
|
+
maxResults: z.number().int().min(1).max(MAX_SEARCH_LIMIT).optional(),
|
|
680
|
+
limit: z.number().int().min(1).max(MAX_SEARCH_LIMIT).optional(),
|
|
681
|
+
maxScopes: z.number().int().min(1).max(MAX_SEARCH_SCOPES).optional(),
|
|
682
|
+
timeoutMs: z.number().int().min(1000).max(MAX_SEARCH_TIMEOUT_MS).optional(),
|
|
683
|
+
maxBytes: z.number().int().min(1).max(MAX_SEARCH_MAX_BYTES).optional(),
|
|
684
|
+
},
|
|
685
|
+
async handler(args, { connection, readClient }) {
|
|
686
|
+
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
687
|
+
if (!query) {
|
|
688
|
+
return textResult({ error: "query is required and must be a non-empty string" }, true);
|
|
689
|
+
}
|
|
690
|
+
if (query.length > SEARCH_QUERY_MAX_CHARS) {
|
|
691
|
+
return textResult({
|
|
692
|
+
error: "query_too_long",
|
|
693
|
+
maxQueryChars: SEARCH_QUERY_MAX_CHARS,
|
|
694
|
+
}, true);
|
|
695
|
+
}
|
|
696
|
+
const normalizedScopes = normalizeRequestedScopes(args.scopes, connection);
|
|
697
|
+
const requestedScopes = normalizedScopes.requestedScopes;
|
|
698
|
+
// maxResults takes precedence over deprecated limit for forward compat
|
|
699
|
+
const limit = clampInteger(args.maxResults ?? args.limit, DEFAULT_SEARCH_LIMIT, 1, MAX_SEARCH_LIMIT);
|
|
700
|
+
const maxScopes = clampInteger(args.maxScopes, DEFAULT_SEARCH_MAX_SCOPES, 1, MAX_SEARCH_SCOPES);
|
|
701
|
+
const timeoutMs = clampInteger(args.timeoutMs, DEFAULT_SEARCH_TIMEOUT_MS, 1000, MAX_SEARCH_TIMEOUT_MS);
|
|
702
|
+
const maxBytes = clampInteger(args.maxBytes, DEFAULT_SEARCH_MAX_BYTES, 1, MAX_SEARCH_MAX_BYTES);
|
|
703
|
+
const totalTimeoutMs = Math.min(MAX_SEARCH_TOTAL_TIMEOUT_MS, timeoutMs);
|
|
704
|
+
const startedAt = Date.now();
|
|
705
|
+
const deadline = startedAt + totalTimeoutMs;
|
|
706
|
+
const discoveryTimeoutMs = Math.min(totalTimeoutMs, DEFAULT_SEARCH_DISCOVERY_TIMEOUT_MS);
|
|
707
|
+
// Decode continuation cursor if provided
|
|
708
|
+
const resumeCursor = typeof args.cursor === "string" ? decodeSearchCursor(args.cursor) : null;
|
|
709
|
+
const resolved = await resolveSearchScopes({
|
|
710
|
+
connection,
|
|
711
|
+
maxScopes,
|
|
712
|
+
discoveryTimeoutMs,
|
|
713
|
+
readClient,
|
|
714
|
+
requestedScopes,
|
|
715
|
+
});
|
|
716
|
+
const matches = [];
|
|
717
|
+
const searchedScopes = [];
|
|
718
|
+
const truncatedScopes = [];
|
|
719
|
+
const errors = [...resolved.errors];
|
|
720
|
+
// When resuming, start from the scope index encoded in the cursor
|
|
721
|
+
const startScopeIndex = resumeCursor
|
|
722
|
+
? Math.max(0, Math.min(resumeCursor.scopeIndex, resolved.scopes.length))
|
|
723
|
+
: 0;
|
|
724
|
+
const initialBlockCursor = resumeCursor?.blockCursor ?? undefined;
|
|
725
|
+
let nextSearchCursor;
|
|
726
|
+
for (let scopeIndex = startScopeIndex; scopeIndex < resolved.scopes.length; scopeIndex += 1) {
|
|
727
|
+
const scope = resolved.scopes[scopeIndex];
|
|
728
|
+
const remainingBeforeScope = deadline - Date.now();
|
|
729
|
+
if (remainingBeforeScope <= 0) {
|
|
730
|
+
// Budget exhausted — offer cursor to continue from this scope
|
|
731
|
+
nextSearchCursor = encodeSearchCursor({ scopeIndex });
|
|
732
|
+
errors.push({ scope, error: "search_total_timeout" });
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
const grant = resolveGrantForScope(connection, scope);
|
|
736
|
+
if (!grant)
|
|
737
|
+
continue;
|
|
738
|
+
if (!resumeCursor && typeof readClient.searchScopeIndex === "function") {
|
|
739
|
+
const remainingMs = deadline - Date.now();
|
|
740
|
+
if (remainingMs <= 0) {
|
|
741
|
+
nextSearchCursor = encodeSearchCursor({ scopeIndex });
|
|
742
|
+
errors.push({ scope, error: "search_total_timeout" });
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
const remainingScopes = resolved.scopes.length - scopeIndex;
|
|
746
|
+
const perScopeTimeoutMs = Math.min(Math.max(250, Math.floor(remainingMs / Math.max(1, remainingScopes))), remainingMs);
|
|
747
|
+
try {
|
|
748
|
+
const indexed = await withTimeout(readClient.searchScopeIndex({
|
|
749
|
+
scope,
|
|
750
|
+
grantId: grant.grantId,
|
|
751
|
+
query,
|
|
752
|
+
maxResults: limit - matches.length,
|
|
753
|
+
}), perScopeTimeoutMs, `search index for ${scope}`);
|
|
754
|
+
if (indexed.status === "hit") {
|
|
755
|
+
searchedScopes.push(scope);
|
|
756
|
+
for (const hit of indexed.hits) {
|
|
757
|
+
if (matches.length >= limit)
|
|
758
|
+
break;
|
|
759
|
+
matches.push({
|
|
760
|
+
scope,
|
|
761
|
+
grantId: grant.grantId,
|
|
762
|
+
preview: hit.preview ?? hit.title ?? "",
|
|
763
|
+
searchedChars: 0,
|
|
764
|
+
truncated: false,
|
|
765
|
+
blockRef: hit.blockRef,
|
|
766
|
+
score: hit.score,
|
|
767
|
+
terms: hit.terms,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
if (matches.length >= limit) {
|
|
771
|
+
const nextScopeIndex = scopeIndex + 1;
|
|
772
|
+
if (nextScopeIndex < resolved.scopes.length) {
|
|
773
|
+
nextSearchCursor = encodeSearchCursor({
|
|
774
|
+
scopeIndex: nextScopeIndex,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
errors.push({
|
|
784
|
+
scope,
|
|
785
|
+
error: err instanceof OperationTimeoutError
|
|
786
|
+
? "scope_index_timeout"
|
|
787
|
+
: "scope_index_failed",
|
|
788
|
+
bodyPreview: stringifyPreview(err instanceof OperationTimeoutError
|
|
789
|
+
? err.message
|
|
790
|
+
: "unexpected scope index failure"),
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Use block cursor from resume only for the first scope we process
|
|
795
|
+
let cursor = scopeIndex === startScopeIndex ? initialBlockCursor : undefined;
|
|
796
|
+
let pageCount = 0;
|
|
797
|
+
let scopeHasMatch = false;
|
|
798
|
+
let scopeTruncated = false;
|
|
799
|
+
let scopeSearched = false;
|
|
800
|
+
try {
|
|
801
|
+
while (pageCount < SEARCH_MAX_PAGES_PER_SCOPE) {
|
|
802
|
+
const remainingMs = deadline - Date.now();
|
|
803
|
+
if (remainingMs <= 0) {
|
|
804
|
+
// Budget hit mid-scope — encode cursor pointing at this scope + block
|
|
805
|
+
nextSearchCursor = encodeSearchCursor({
|
|
806
|
+
scopeIndex,
|
|
807
|
+
blockCursor: cursor,
|
|
808
|
+
});
|
|
809
|
+
errors.push({ scope, error: "search_total_timeout" });
|
|
810
|
+
scopeTruncated = Boolean(cursor);
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
pageCount += 1;
|
|
814
|
+
if (!scopeSearched) {
|
|
815
|
+
searchedScopes.push(scope);
|
|
816
|
+
scopeSearched = true;
|
|
817
|
+
}
|
|
818
|
+
const remainingScopes = resolved.scopes.length - scopeIndex;
|
|
819
|
+
const perScopeTimeoutMs = Math.min(Math.max(250, Math.floor(remainingMs / Math.max(1, remainingScopes))), remainingMs);
|
|
820
|
+
const result = await withTimeout(readClient.readScopeBlocks({
|
|
821
|
+
scope,
|
|
822
|
+
grantId: grant.grantId,
|
|
823
|
+
cursor,
|
|
824
|
+
maxBytes,
|
|
825
|
+
}), perScopeTimeoutMs, `read blocks for ${scope}`);
|
|
826
|
+
const pageText = blockPageSearchText(result.blocks);
|
|
827
|
+
const { documents, textById } = blockPageSearchDocuments(scope, result.blocks);
|
|
828
|
+
const pageHits = documents.length > 0
|
|
829
|
+
? MiniSearchIndex.build(documents).search({ query, limit: 1 })
|
|
830
|
+
: [];
|
|
831
|
+
const pageHit = pageHits[0];
|
|
832
|
+
if (pageHit && matches.length < limit && !scopeHasMatch) {
|
|
833
|
+
const hitText = textById.get(pageHit.id) ?? pageText;
|
|
834
|
+
matches.push({
|
|
835
|
+
scope,
|
|
836
|
+
grantId: grant.grantId,
|
|
837
|
+
collectedAt: result.collectedAt,
|
|
838
|
+
preview: previewMatch(hitText, miniSearchMatchIndex(hitText, pageHit.terms)),
|
|
839
|
+
searchedChars: pageText.length,
|
|
840
|
+
truncated: Boolean(result.nextCursor),
|
|
841
|
+
});
|
|
842
|
+
scopeHasMatch = true;
|
|
843
|
+
}
|
|
844
|
+
if (result.nextCursor) {
|
|
845
|
+
cursor = result.nextCursor;
|
|
846
|
+
if (pageCount >= SEARCH_MAX_PAGES_PER_SCOPE ||
|
|
847
|
+
matches.length >= limit) {
|
|
848
|
+
scopeTruncated = true;
|
|
849
|
+
// More pages remain in this scope — cursor points here
|
|
850
|
+
nextSearchCursor = encodeSearchCursor({
|
|
851
|
+
scopeIndex,
|
|
852
|
+
blockCursor: cursor,
|
|
853
|
+
});
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
if (scopeTruncated) {
|
|
861
|
+
truncatedScopes.push(scope);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
catch (err) {
|
|
865
|
+
errors.push({
|
|
866
|
+
scope,
|
|
867
|
+
error: err instanceof OperationTimeoutError
|
|
868
|
+
? "scope_search_timeout"
|
|
869
|
+
: "scope_read_failed",
|
|
870
|
+
status: err instanceof McpDataReadError ? err.status : undefined,
|
|
871
|
+
bodyPreview: stringifyPreview(err instanceof McpDataReadError
|
|
872
|
+
? err.body
|
|
873
|
+
: err instanceof OperationTimeoutError
|
|
874
|
+
? err.message
|
|
875
|
+
: "unexpected scope read failure"),
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
if (matches.length >= limit) {
|
|
879
|
+
// Result limit hit — if more scopes remain, offer cursor for next scope
|
|
880
|
+
const nextScopeIndex = scopeIndex + 1;
|
|
881
|
+
if (nextScopeIndex < resolved.scopes.length) {
|
|
882
|
+
nextSearchCursor = encodeSearchCursor({
|
|
883
|
+
scopeIndex: nextScopeIndex,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const elapsedMs = Date.now() - startedAt;
|
|
890
|
+
return textResult({
|
|
891
|
+
query,
|
|
892
|
+
results: matches,
|
|
893
|
+
// Backward-compatible alias for existing clients/tests. `results` is the
|
|
894
|
+
// canonical field, but removing `matches` is a separate migration.
|
|
895
|
+
matches,
|
|
896
|
+
searchedScopes,
|
|
897
|
+
skippedScopes: [
|
|
898
|
+
...normalizedScopes.skippedScopes,
|
|
899
|
+
...resolved.skippedScopes,
|
|
900
|
+
],
|
|
901
|
+
truncatedScopes,
|
|
902
|
+
errors,
|
|
903
|
+
...(nextSearchCursor ? { nextSearchCursor } : {}),
|
|
904
|
+
elapsedMs,
|
|
905
|
+
limits: {
|
|
906
|
+
maxResults: limit,
|
|
907
|
+
maxScopes,
|
|
908
|
+
maxRequestedScopes: SEARCH_REQUESTED_SCOPES_LIMIT,
|
|
909
|
+
timeoutMs,
|
|
910
|
+
discoveryTimeoutMs,
|
|
911
|
+
queryChars: SEARCH_QUERY_MAX_CHARS,
|
|
912
|
+
scopeChars: SEARCH_SCOPE_MAX_CHARS,
|
|
913
|
+
previewChars: SEARCH_PREVIEW_CHARS,
|
|
914
|
+
requestedBytesPerPage: maxBytes,
|
|
915
|
+
maxPagesPerScope: SEARCH_MAX_PAGES_PER_SCOPE,
|
|
916
|
+
totalTimeoutMs,
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
const getScopeFile = {
|
|
922
|
+
name: "get_scope_file",
|
|
923
|
+
title: "Get scope file",
|
|
924
|
+
description: "Fetch an approved file/PDF scope. Defaults to metadata plus a resource link; set includeContent=true only when the client supports inline resource blobs. Use search_personal_context for extracted text.",
|
|
925
|
+
inputSchema: {
|
|
926
|
+
...rawScopeFileInputSchema,
|
|
927
|
+
timeoutMs: z
|
|
928
|
+
.number()
|
|
929
|
+
.int()
|
|
930
|
+
.min(1000)
|
|
931
|
+
.max(MAX_READ_FILE_TIMEOUT_MS)
|
|
932
|
+
.optional(),
|
|
933
|
+
},
|
|
934
|
+
async handler(args, { connection, readClient }) {
|
|
935
|
+
const scope = typeof args.scope === "string" ? args.scope : null;
|
|
936
|
+
if (!scope) {
|
|
937
|
+
return textResult({ error: "scope is required and must be a string" }, true);
|
|
938
|
+
}
|
|
939
|
+
const grant = resolveGrantForScope(connection, scope);
|
|
940
|
+
if (!grant) {
|
|
941
|
+
return textResult({
|
|
942
|
+
error: "scope_not_granted",
|
|
943
|
+
message: `Scope '${scope}' is not covered by any grant on this MCP connection.`,
|
|
944
|
+
grantedScopes: uniqueScopes(connection),
|
|
945
|
+
}, true);
|
|
946
|
+
}
|
|
947
|
+
const at = typeof args.at === "string" ? args.at : undefined;
|
|
948
|
+
const fileId = typeof args.fileId === "string" ? args.fileId : undefined;
|
|
949
|
+
const includeContent = typeof args.includeContent === "boolean" ? args.includeContent : false;
|
|
950
|
+
const maxBytes = clampInteger(args.maxBytes, DEFAULT_READ_FILE_MAX_BYTES, 1, MAX_READ_FILE_MAX_BYTES);
|
|
951
|
+
const timeoutMs = clampInteger(args.timeoutMs, DEFAULT_READ_FILE_TIMEOUT_MS, 1000, MAX_READ_FILE_TIMEOUT_MS);
|
|
952
|
+
const resourceUri = rawScopeResourceUri({ scope, at, fileId });
|
|
953
|
+
try {
|
|
954
|
+
const raw = await withTimeout(readClient.readRawScopeFile({
|
|
955
|
+
scope,
|
|
956
|
+
grantId: grant.grantId,
|
|
957
|
+
at,
|
|
958
|
+
fileId,
|
|
959
|
+
}), timeoutMs, `read raw file for ${scope}`);
|
|
960
|
+
const metadata = rawScopeMetadata(raw);
|
|
961
|
+
const base = {
|
|
962
|
+
...metadata,
|
|
963
|
+
grantId: grant.grantId,
|
|
964
|
+
resourceUri,
|
|
965
|
+
includeContent,
|
|
966
|
+
maxBytes,
|
|
967
|
+
timeoutMs,
|
|
968
|
+
};
|
|
969
|
+
if (!includeContent || raw.sizeBytes > maxBytes) {
|
|
970
|
+
return {
|
|
971
|
+
content: [
|
|
972
|
+
{
|
|
973
|
+
type: "text",
|
|
974
|
+
text: JSON.stringify({
|
|
975
|
+
...base,
|
|
976
|
+
contentIncluded: false,
|
|
977
|
+
reason: !includeContent
|
|
978
|
+
? "includeContent=false"
|
|
979
|
+
: "file exceeds maxBytes for inline MCP tool response",
|
|
980
|
+
nextStep: "Use resources/read with resourceUri if your MCP client supports resources, or retry with a higher maxBytes if appropriate.",
|
|
981
|
+
}, null, 2),
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
type: "resource_link",
|
|
985
|
+
uri: resourceUri,
|
|
986
|
+
name: raw.filename ?? scope,
|
|
987
|
+
title: raw.filename ?? scope,
|
|
988
|
+
description: `Raw file for ${scope}`,
|
|
989
|
+
mimeType: raw.mimeType,
|
|
990
|
+
size: raw.sizeBytes,
|
|
991
|
+
},
|
|
992
|
+
],
|
|
993
|
+
structuredContent: {
|
|
994
|
+
...base,
|
|
995
|
+
contentIncluded: false,
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
content: [
|
|
1001
|
+
{
|
|
1002
|
+
type: "text",
|
|
1003
|
+
text: JSON.stringify({
|
|
1004
|
+
...base,
|
|
1005
|
+
contentIncluded: true,
|
|
1006
|
+
resourceType: "blob",
|
|
1007
|
+
}, null, 2),
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
type: "resource",
|
|
1011
|
+
resource: {
|
|
1012
|
+
uri: resourceUri,
|
|
1013
|
+
mimeType: raw.mimeType,
|
|
1014
|
+
blob: raw.contentBase64,
|
|
1015
|
+
_meta: metadata,
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
],
|
|
1019
|
+
structuredContent: {
|
|
1020
|
+
...base,
|
|
1021
|
+
contentIncluded: true,
|
|
1022
|
+
resourceType: "blob",
|
|
1023
|
+
},
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
catch (err) {
|
|
1027
|
+
if (err instanceof OperationTimeoutError) {
|
|
1028
|
+
return textResult({
|
|
1029
|
+
error: "scope_file_timeout",
|
|
1030
|
+
message: err.message,
|
|
1031
|
+
timeoutMs: err.timeoutMs,
|
|
1032
|
+
}, true);
|
|
1033
|
+
}
|
|
1034
|
+
if (err instanceof McpDataReadError) {
|
|
1035
|
+
return textResult({
|
|
1036
|
+
error: err.status === 404
|
|
1037
|
+
? "scope_file_not_found"
|
|
1038
|
+
: "scope_file_read_failed",
|
|
1039
|
+
status: err.status,
|
|
1040
|
+
body: err.body,
|
|
1041
|
+
}, true);
|
|
1042
|
+
}
|
|
1043
|
+
return textResult({
|
|
1044
|
+
error: "tool_handler_error",
|
|
1045
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1046
|
+
}, true);
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
export const MCP_TOOLS = [
|
|
1051
|
+
listGrantedSources,
|
|
1052
|
+
listGrantedScopes,
|
|
1053
|
+
requestScopeAccess,
|
|
1054
|
+
readScope,
|
|
1055
|
+
getScopeFile,
|
|
1056
|
+
searchPersonalContext,
|
|
1057
|
+
];
|
|
1058
|
+
//# sourceMappingURL=tools.js.map
|