@bitkyc08/opencodex 2.8.0 → 2.9.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 +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +126 -10
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +143 -17
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/init.ts +129 -102
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +14 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +60 -14
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +179 -13
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +97 -25
- package/src/types.ts +39 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
|
@@ -2,6 +2,16 @@ import http2 from "node:http2";
|
|
|
2
2
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
3
3
|
import { namespacedToolName, type OcxProviderConfig, type OcxUsage } from "../../types";
|
|
4
4
|
import { CONNECT_FLAG_END_STREAM, decodeAvailableConnectFrames, encodeConnectFrame } from "./framing";
|
|
5
|
+
import {
|
|
6
|
+
CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
|
|
7
|
+
CURSOR_MAX_CONNECT_FRAME_BYTES,
|
|
8
|
+
CURSOR_MAX_PENDING_FRAMES,
|
|
9
|
+
CURSOR_PENDING_FRAMES_RESUME,
|
|
10
|
+
CURSOR_TRANSPORT_MAX_BUFFERED_BYTES,
|
|
11
|
+
CURSOR_TRANSPORT_RESUME_BYTES,
|
|
12
|
+
TranslatorBudgetExceededError,
|
|
13
|
+
type TranslatorBudget,
|
|
14
|
+
} from "../../lib/translator-budget";
|
|
5
15
|
import { activePromptText, prepareCursorRunRequest } from "./protobuf-request";
|
|
6
16
|
import {
|
|
7
17
|
createCursorContextUsageTracker,
|
|
@@ -41,7 +51,13 @@ import { debugProviderDiagnostic } from "../../lib/debug";
|
|
|
41
51
|
import { classifyCursorError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
|
|
42
52
|
import { mcpArgsFromToolCall } from "./protobuf-events";
|
|
43
53
|
import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
|
|
44
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
handleCursorNativeExec,
|
|
56
|
+
handleCursorNativeKv,
|
|
57
|
+
releaseCursorBlobRequestScope,
|
|
58
|
+
type CursorBlobRequestScopeToken,
|
|
59
|
+
type CursorNativeExecContext,
|
|
60
|
+
} from "./native-exec";
|
|
45
61
|
import { effectiveCursorNativeExecAllow } from "./exec-policy";
|
|
46
62
|
import { resolveMcpServers } from "./mcp-config";
|
|
47
63
|
import { CursorMcpManager } from "./mcp-manager";
|
|
@@ -58,6 +74,10 @@ import {
|
|
|
58
74
|
requestedCursorToolUseCount,
|
|
59
75
|
} from "./tool-definitions";
|
|
60
76
|
import type { CursorNativeToolDeps } from "./native-exec-tools";
|
|
77
|
+
import {
|
|
78
|
+
terminateBackgroundShellsForSession,
|
|
79
|
+
type BackgroundShellTerminationReport,
|
|
80
|
+
} from "./native-exec-shell";
|
|
61
81
|
import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
|
|
62
82
|
import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
|
|
63
83
|
|
|
@@ -394,9 +414,15 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
394
414
|
private activeClientToolFinalizeGraceMs: number;
|
|
395
415
|
private readonly token: string;
|
|
396
416
|
private readonly mcpManager?: CursorMcpManager;
|
|
417
|
+
private readonly translatorBudget: TranslatorBudget;
|
|
418
|
+
private pendingTransportFrames = 0;
|
|
419
|
+
private transportBufferedBytes = 0;
|
|
397
420
|
private readonly desktopDeps: CursorNativeToolDeps;
|
|
398
421
|
private execContext: CursorNativeExecContext = {};
|
|
399
422
|
private mcpPrepared?: Promise<void>;
|
|
423
|
+
private releaseMcpObservation?: () => void;
|
|
424
|
+
private blobRequestScope?: CursorBlobRequestScopeToken;
|
|
425
|
+
private shellCleanup?: Promise<BackgroundShellTerminationReport>;
|
|
400
426
|
// Per-turn diagnostic counters/timestamps when provider debug is on (`ocx debug provider on`). Stamped in open(), cleared on
|
|
401
427
|
// close; safe to read after a stream failure because open() owns the only writer before run().
|
|
402
428
|
private turnStartedAt = 0;
|
|
@@ -407,6 +433,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
407
433
|
private readonly sessionId = crypto.randomUUID();
|
|
408
434
|
|
|
409
435
|
constructor(private readonly input: CursorTransportFactoryInput) {
|
|
436
|
+
this.translatorBudget = input.translatorBudget;
|
|
410
437
|
this.token = resolveCursorToken(input.provider, input.headers);
|
|
411
438
|
// Grace window before a drained client-tool turn is finalized. Small enough not to look like a
|
|
412
439
|
// stall, large enough to catch a sibling tool call announced in the next receive chunk. Injectable
|
|
@@ -415,11 +442,18 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
415
442
|
this.activeClientToolFinalizeGraceMs = this.clientToolFinalizeGraceMs;
|
|
416
443
|
// Desktop (computer-use / record-screen) executors are available even with no MCP servers.
|
|
417
444
|
this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
|
|
418
|
-
this.execContext = {
|
|
445
|
+
this.execContext = {
|
|
446
|
+
...this.desktopDeps,
|
|
447
|
+
sessionId: this.sessionId,
|
|
448
|
+
unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(input.provider, input.requestDeclaresFullAccess === true),
|
|
449
|
+
};
|
|
419
450
|
const servers = resolveMcpServers(input.provider);
|
|
420
451
|
if (servers.length > 0) {
|
|
421
452
|
this.mcpManager = new CursorMcpManager(servers, {
|
|
422
453
|
log: message => console.warn(message),
|
|
454
|
+
maxTools: input.provider.mcpMaxTools,
|
|
455
|
+
maxSchemaBytes: input.provider.mcpMaxSchemaBytes,
|
|
456
|
+
maxResultBytes: input.provider.mcpMaxResultBytes,
|
|
423
457
|
});
|
|
424
458
|
}
|
|
425
459
|
}
|
|
@@ -436,10 +470,16 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
436
470
|
this.mcpPrepared = (async () => {
|
|
437
471
|
try {
|
|
438
472
|
const mcpToolDefs = await buildMcpToolDefinitions(this.mcpManager!);
|
|
473
|
+
this.releaseMcpObservation?.();
|
|
474
|
+
this.releaseMcpObservation = this.translatorBudget.observeExternallyCapped(
|
|
475
|
+
"mcp_payload",
|
|
476
|
+
new TextEncoder().encode(JSON.stringify(mcpToolDefs)).byteLength,
|
|
477
|
+
);
|
|
439
478
|
this.execContext = {
|
|
440
479
|
...this.desktopDeps,
|
|
441
480
|
...mcpDepsFromManager(this.mcpManager!),
|
|
442
481
|
mcpToolDefs,
|
|
482
|
+
sessionId: this.sessionId,
|
|
443
483
|
unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(this.input.provider, this.input.requestDeclaresFullAccess === true),
|
|
444
484
|
};
|
|
445
485
|
} catch (err) {
|
|
@@ -455,11 +495,11 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
455
495
|
}
|
|
456
496
|
|
|
457
497
|
async *run(request: CursorRunRequest, signal?: AbortSignal): AsyncIterable<CursorServerMessage> {
|
|
458
|
-
const queue: CursorServerMessage
|
|
498
|
+
const queue: Array<{ message: CursorServerMessage; bytes: number }> = [];
|
|
459
499
|
let notify: (() => void) | undefined;
|
|
460
500
|
let done = false;
|
|
461
501
|
let failure: Error | undefined;
|
|
462
|
-
let state = createCursorProtobufEventState();
|
|
502
|
+
let state = createCursorProtobufEventState({ translatorBudget: this.translatorBudget });
|
|
463
503
|
let failureLogged = false;
|
|
464
504
|
// One per-turn summary of the failure path (end-stream error, socket reset, abort) so the
|
|
465
505
|
// operator can see how far the turn got and how it was classified without re-scanning every
|
|
@@ -488,7 +528,9 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
488
528
|
};
|
|
489
529
|
|
|
490
530
|
const push = (message: CursorServerMessage) => {
|
|
491
|
-
|
|
531
|
+
const bytes = new TextEncoder().encode(JSON.stringify(message)).byteLength;
|
|
532
|
+
this.reserveTransportBytes(bytes);
|
|
533
|
+
queue.push({ message, bytes });
|
|
492
534
|
wake();
|
|
493
535
|
};
|
|
494
536
|
|
|
@@ -522,29 +564,40 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
522
564
|
const prepared = prepareCursorRunRequest(request, {
|
|
523
565
|
estimateInputTokens: contextUsage.carryForwardTokens === undefined,
|
|
524
566
|
});
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
567
|
+
this.blobRequestScope = prepared.blobRequestScope;
|
|
568
|
+
try {
|
|
569
|
+
state = createCursorProtobufEventState({
|
|
570
|
+
clientToolNames: clientToolDefs.map(tool => tool.toolName || tool.name),
|
|
571
|
+
parallelToolCalls: request.parallelToolCalls,
|
|
572
|
+
toolSchemas,
|
|
573
|
+
cursorToolNameMap,
|
|
574
|
+
translatorBudget: this.translatorBudget,
|
|
575
|
+
contextUsage,
|
|
576
|
+
...(prepared.estimatedInputTokens !== undefined
|
|
577
|
+
? { estimatedInputTokens: prepared.estimatedInputTokens }
|
|
578
|
+
: {}),
|
|
579
|
+
});
|
|
580
|
+
this.open(prepared.bytes, signal, state, push, err => {
|
|
581
|
+
this.releaseBlobRequestScope();
|
|
582
|
+
failure = err;
|
|
583
|
+
wake();
|
|
584
|
+
}, () => {
|
|
585
|
+
this.releaseBlobRequestScope();
|
|
586
|
+
done = true;
|
|
587
|
+
wake();
|
|
588
|
+
});
|
|
589
|
+
} catch (error) {
|
|
590
|
+
this.releaseBlobRequestScope();
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
543
593
|
|
|
544
594
|
while (!done || queue.length > 0) {
|
|
545
595
|
while (queue.length > 0) {
|
|
546
|
-
const
|
|
547
|
-
if (
|
|
596
|
+
const queued = queue.shift();
|
|
597
|
+
if (queued) {
|
|
598
|
+
this.releaseTransportBytes(queued.bytes);
|
|
599
|
+
yield queued.message;
|
|
600
|
+
}
|
|
548
601
|
}
|
|
549
602
|
if (failure) {
|
|
550
603
|
// A CANCEL is benign only on the client-tool suspend path (expectedClose); an
|
|
@@ -565,6 +618,35 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
565
618
|
|
|
566
619
|
writeClient(_message: CursorClientMessage): void {}
|
|
567
620
|
|
|
621
|
+
private reserveTransportBytes(bytes: number): void {
|
|
622
|
+
if (this.transportBufferedBytes + bytes > CURSOR_TRANSPORT_MAX_BUFFERED_BYTES) {
|
|
623
|
+
throw new TranslatorBudgetExceededError("cursor_transport", CURSOR_TRANSPORT_MAX_BUFFERED_BYTES);
|
|
624
|
+
}
|
|
625
|
+
this.translatorBudget.chargeRetained(bytes, { kind: "cursor_transport" });
|
|
626
|
+
this.transportBufferedBytes += bytes;
|
|
627
|
+
this.updateTransportFlowControl();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private releaseTransportBytes(bytes: number): void {
|
|
631
|
+
this.transportBufferedBytes = Math.max(0, this.transportBufferedBytes - bytes);
|
|
632
|
+
this.translatorBudget.releaseRetained(bytes, { kind: "cursor_transport" });
|
|
633
|
+
this.updateTransportFlowControl();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private updateTransportFlowControl(): void {
|
|
637
|
+
if (
|
|
638
|
+
this.transportBufferedBytes >= CURSOR_TRANSPORT_MAX_BUFFERED_BYTES
|
|
639
|
+
|| this.pendingTransportFrames >= CURSOR_MAX_PENDING_FRAMES
|
|
640
|
+
) {
|
|
641
|
+
this.stream?.pause();
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (
|
|
645
|
+
this.transportBufferedBytes <= CURSOR_TRANSPORT_RESUME_BYTES
|
|
646
|
+
&& this.pendingTransportFrames <= CURSOR_PENDING_FRAMES_RESUME
|
|
647
|
+
) this.stream?.resume();
|
|
648
|
+
}
|
|
649
|
+
|
|
568
650
|
requestCommitted(): boolean {
|
|
569
651
|
return this.committed;
|
|
570
652
|
}
|
|
@@ -576,13 +658,21 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
576
658
|
}
|
|
577
659
|
}
|
|
578
660
|
|
|
579
|
-
|
|
661
|
+
private startShellCleanup(): Promise<BackgroundShellTerminationReport> {
|
|
662
|
+
return this.shellCleanup ??= terminateBackgroundShellsForSession(this.sessionId);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async close(): Promise<void> {
|
|
580
666
|
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
581
667
|
this.clearPendingFinalize();
|
|
582
668
|
this.clearFirstFrameTimer();
|
|
583
669
|
this.stream?.close();
|
|
584
670
|
this.session?.close();
|
|
671
|
+
this.releaseBlobRequestScope();
|
|
672
|
+
this.releaseMcpObservation?.();
|
|
673
|
+
this.releaseMcpObservation = undefined;
|
|
585
674
|
void this.mcpManager?.dispose();
|
|
675
|
+
await this.startShellCleanup();
|
|
586
676
|
}
|
|
587
677
|
|
|
588
678
|
private cancelCursorRun(): void {
|
|
@@ -596,7 +686,18 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
596
686
|
this.stream?.destroy();
|
|
597
687
|
}
|
|
598
688
|
this.session?.close();
|
|
689
|
+
this.releaseBlobRequestScope();
|
|
690
|
+
this.releaseMcpObservation?.();
|
|
691
|
+
this.releaseMcpObservation = undefined;
|
|
599
692
|
void this.mcpManager?.dispose();
|
|
693
|
+
void this.startShellCleanup().catch(() => { /* close() observes the same cleanup promise */ });
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private releaseBlobRequestScope(): void {
|
|
697
|
+
const scope = this.blobRequestScope;
|
|
698
|
+
if (!scope) return;
|
|
699
|
+
this.blobRequestScope = undefined;
|
|
700
|
+
releaseCursorBlobRequestScope(scope);
|
|
600
701
|
}
|
|
601
702
|
|
|
602
703
|
private clearPendingFinalize(): void {
|
|
@@ -725,6 +826,71 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
725
826
|
}, this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS);
|
|
726
827
|
|
|
727
828
|
let pending: Uint8Array<ArrayBufferLike> = new Uint8Array();
|
|
829
|
+
let frameWork: Promise<void> = Promise.resolve();
|
|
830
|
+
const reservePayloadCopy = (bytes: number) => {
|
|
831
|
+
if (this.transportBufferedBytes + bytes > CURSOR_TRANSPORT_MAX_BUFFERED_BYTES) {
|
|
832
|
+
throw new TranslatorBudgetExceededError("cursor_transport", CURSOR_TRANSPORT_MAX_BUFFERED_BYTES);
|
|
833
|
+
}
|
|
834
|
+
const reservation = this.translatorBudget.reserveTransient(bytes, { kind: "cursor_transport" });
|
|
835
|
+
this.transportBufferedBytes += bytes;
|
|
836
|
+
this.updateTransportFlowControl();
|
|
837
|
+
return {
|
|
838
|
+
commitRetained: () => reservation.commitRetained(),
|
|
839
|
+
release: () => {
|
|
840
|
+
reservation.release();
|
|
841
|
+
this.transportBufferedBytes = Math.max(0, this.transportBufferedBytes - bytes);
|
|
842
|
+
this.updateTransportFlowControl();
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
};
|
|
846
|
+
const handleFrame = async (frame: ReturnType<typeof decodeAvailableConnectFrames>["frames"][number]) => {
|
|
847
|
+
this.framesReceived++;
|
|
848
|
+
if ((frame.flags & CONNECT_FLAG_END_STREAM) === CONNECT_FLAG_END_STREAM) {
|
|
849
|
+
const endError = parseConnectEndStreamError(frame.payload);
|
|
850
|
+
debugProviderDiagnostic("cursor", "connect-end-stream", endError ? {
|
|
851
|
+
code: cursorConnectErrorCode(frame.payload),
|
|
852
|
+
message: redactCursorForLog(endError.message),
|
|
853
|
+
classified: classifyCursorError(endError.message),
|
|
854
|
+
framesReceived: this.framesReceived,
|
|
855
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
856
|
+
} : { framesReceived: this.framesReceived, elapsedMs: Date.now() - this.turnStartedAt });
|
|
857
|
+
if (endError) failAndClear(endError);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
await this.handleServerMessage(fromBinary(AgentServerMessageSchema, frame.payload), state, push);
|
|
861
|
+
};
|
|
862
|
+
const drainPendingFrames = () => {
|
|
863
|
+
const availableSlots = CURSOR_MAX_PENDING_FRAMES - this.pendingTransportFrames;
|
|
864
|
+
if (availableSlots <= 0 || pending.byteLength === 0) {
|
|
865
|
+
this.updateTransportFlowControl();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const previousPayloadBytes = connectBufferedPayloadBytes(pending);
|
|
869
|
+
// The decoder materializes payload and residual copies. Keep the aggregate pending owner
|
|
870
|
+
// charged until every replacement has been admitted and committed; a failed admission must
|
|
871
|
+
// leave that predecessor lease intact for deterministic cleanup.
|
|
872
|
+
const decoded = decodeAvailableConnectFrames(
|
|
873
|
+
pending,
|
|
874
|
+
CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
|
|
875
|
+
availableSlots,
|
|
876
|
+
reservePayloadCopy,
|
|
877
|
+
);
|
|
878
|
+
pending = decoded.remainder;
|
|
879
|
+
this.releaseTransportBytes(previousPayloadBytes);
|
|
880
|
+
for (const frame of decoded.frames) {
|
|
881
|
+
this.pendingTransportFrames += 1;
|
|
882
|
+
this.updateTransportFlowControl();
|
|
883
|
+
frameWork = frameWork
|
|
884
|
+
.then(() => handleFrame(frame))
|
|
885
|
+
.catch(err => failAndClear(err instanceof Error ? err : new Error(String(err))))
|
|
886
|
+
.finally(() => {
|
|
887
|
+
this.releaseTransportBytes(frame.payload.byteLength);
|
|
888
|
+
this.pendingTransportFrames = Math.max(0, this.pendingTransportFrames - 1);
|
|
889
|
+
this.updateTransportFlowControl();
|
|
890
|
+
drainPendingFrames();
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
};
|
|
728
894
|
this.stream.on("data", chunk => {
|
|
729
895
|
this.clearFirstFrameTimer();
|
|
730
896
|
if (!this.firstFrameLogged) {
|
|
@@ -733,30 +899,26 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
733
899
|
debugProviderDiagnostic("cursor", "first-frame", { latencyMs: this.firstFrameAt - this.turnStartedAt });
|
|
734
900
|
}
|
|
735
901
|
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
736
|
-
|
|
902
|
+
let incomingPayloadBytes = 0;
|
|
903
|
+
let incomingCharged = false;
|
|
904
|
+
let replacement: ReturnType<typeof reservePayloadCopy> | undefined;
|
|
737
905
|
try {
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
} : { framesReceived: this.framesReceived, elapsedMs: Date.now() - this.turnStartedAt });
|
|
752
|
-
if (endError) failAndClear(endError);
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
void this.handleServerMessage(fromBinary(AgentServerMessageSchema, frame.payload), state, push).catch(err => {
|
|
756
|
-
failAndClear(err instanceof Error ? err : new Error(String(err)));
|
|
757
|
-
});
|
|
758
|
-
}
|
|
906
|
+
const previousPendingPayloadBytes = connectBufferedPayloadBytes(pending);
|
|
907
|
+
const nextPendingPayloadBytes = connectBufferedPayloadBytesAcross(pending, bytes);
|
|
908
|
+
incomingPayloadBytes = Math.max(0, nextPendingPayloadBytes - previousPendingPayloadBytes);
|
|
909
|
+
this.reserveTransportBytes(incomingPayloadBytes);
|
|
910
|
+
incomingCharged = true;
|
|
911
|
+
replacement = reservePayloadCopy(nextPendingPayloadBytes);
|
|
912
|
+
const nextPending = concatBytes(pending, bytes);
|
|
913
|
+
pending = nextPending;
|
|
914
|
+
replacement.commitRetained();
|
|
915
|
+
replacement = undefined;
|
|
916
|
+
this.releaseTransportBytes(previousPendingPayloadBytes + incomingPayloadBytes);
|
|
917
|
+
incomingCharged = false;
|
|
918
|
+
drainPendingFrames();
|
|
759
919
|
} catch (err) {
|
|
920
|
+
replacement?.release();
|
|
921
|
+
if (incomingCharged) this.releaseTransportBytes(incomingPayloadBytes);
|
|
760
922
|
failAndClear(err instanceof Error ? err : new Error(String(err)));
|
|
761
923
|
}
|
|
762
924
|
});
|
|
@@ -824,7 +986,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
824
986
|
if (!this.stream) return;
|
|
825
987
|
debugProviderDiagnostic("cursor", "frame", describeCursorServerFrame(message));
|
|
826
988
|
if (message.message.case === "kvServerMessage") {
|
|
827
|
-
this.stream.write(encodeConnectFrame(handleCursorNativeKv(message.message.value)));
|
|
989
|
+
this.stream.write(encodeConnectFrame(handleCursorNativeKv(message.message.value, this.blobRequestScope)));
|
|
828
990
|
return;
|
|
829
991
|
}
|
|
830
992
|
if (message.message.case === "execServerMessage") {
|
|
@@ -985,6 +1147,40 @@ function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
|
985
1147
|
return out;
|
|
986
1148
|
}
|
|
987
1149
|
|
|
1150
|
+
function connectBufferedPayloadBytes(input: Uint8Array): number {
|
|
1151
|
+
let offset = 0;
|
|
1152
|
+
let payloadBytes = 0;
|
|
1153
|
+
while (input.byteLength - offset >= 5) {
|
|
1154
|
+
const length = new DataView(input.buffer, input.byteOffset + offset, input.byteLength - offset).getUint32(1, false);
|
|
1155
|
+
const available = Math.min(length, input.byteLength - offset - 5);
|
|
1156
|
+
payloadBytes += available;
|
|
1157
|
+
if (available < length) break;
|
|
1158
|
+
offset += 5 + length;
|
|
1159
|
+
}
|
|
1160
|
+
return payloadBytes;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/** Payload-byte count for a virtual concatenation, without allocating the concatenated buffer. */
|
|
1164
|
+
function connectBufferedPayloadBytesAcross(first: Uint8Array, second: Uint8Array): number {
|
|
1165
|
+
const totalBytes = first.byteLength + second.byteLength;
|
|
1166
|
+
const byteAt = (index: number): number => index < first.byteLength
|
|
1167
|
+
? first[index]!
|
|
1168
|
+
: second[index - first.byteLength]!;
|
|
1169
|
+
let offset = 0;
|
|
1170
|
+
let payloadBytes = 0;
|
|
1171
|
+
while (totalBytes - offset >= 5) {
|
|
1172
|
+
const length = (byteAt(offset + 1) * 0x1000000)
|
|
1173
|
+
+ (byteAt(offset + 2) << 16)
|
|
1174
|
+
+ (byteAt(offset + 3) << 8)
|
|
1175
|
+
+ byteAt(offset + 4);
|
|
1176
|
+
const available = Math.min(length, totalBytes - offset - 5);
|
|
1177
|
+
payloadBytes += available;
|
|
1178
|
+
if (available < length) break;
|
|
1179
|
+
offset += 5 + length;
|
|
1180
|
+
}
|
|
1181
|
+
return payloadBytes;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
988
1184
|
/** Host-only label for Cursor transport diagnostics — never leaks path/query/credentials. */
|
|
989
1185
|
function cursorHostLabel(baseUrl: string): string {
|
|
990
1186
|
try {
|
|
@@ -6,6 +6,34 @@ import type { ResolvedMcpServer } from "./mcp-config";
|
|
|
6
6
|
|
|
7
7
|
const DEFAULT_CONNECT_TIMEOUT_MS = 15_000;
|
|
8
8
|
const DEFAULT_CALL_TIMEOUT_MS = 120_000;
|
|
9
|
+
const CURSOR_MCP_MAX_SERVERS = 32;
|
|
10
|
+
const CURSOR_MCP_MAX_TOOLS = 512;
|
|
11
|
+
const CURSOR_MCP_MAX_RESOURCES = 1_024;
|
|
12
|
+
const CURSOR_MCP_MAX_SCHEMA_BYTES = 256 * 1024;
|
|
13
|
+
const CURSOR_MCP_MAX_CATALOG_BYTES = 4 * 1024 * 1024;
|
|
14
|
+
const CURSOR_MCP_MAX_RESULT_BYTES = 8 * 1024 * 1024;
|
|
15
|
+
const utf8 = new TextEncoder();
|
|
16
|
+
|
|
17
|
+
export class McpCatalogLimitError extends Error {
|
|
18
|
+
readonly code = "MCP_CATALOG_LIMIT";
|
|
19
|
+
constructor() { super("Cursor MCP catalog limit exceeded"); this.name = "McpCatalogLimitError"; }
|
|
20
|
+
}
|
|
21
|
+
export class McpPayloadTooLargeError extends Error {
|
|
22
|
+
readonly code = "MCP_PAYLOAD_TOO_LARGE";
|
|
23
|
+
constructor(readonly kind: "result" | "resource") { super(`Cursor MCP ${kind} too large`); this.name = "McpPayloadTooLargeError"; }
|
|
24
|
+
}
|
|
25
|
+
/** Backward-compatible export for existing integrations. */
|
|
26
|
+
export const CursorMcpPayloadTooLargeError = McpPayloadTooLargeError;
|
|
27
|
+
|
|
28
|
+
function stableJson(value: unknown): string {
|
|
29
|
+
return JSON.stringify(value, (_key, nested) => nested && typeof nested === "object" && !Array.isArray(nested)
|
|
30
|
+
? Object.fromEntries(Object.entries(nested as Record<string, unknown>).sort(([left], [right]) => left.localeCompare(right)))
|
|
31
|
+
: nested);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function payloadBytes(value: unknown): number {
|
|
35
|
+
return utf8.encode(stableJson(value)).byteLength;
|
|
36
|
+
}
|
|
9
37
|
|
|
10
38
|
/** A tool discovered on a connected MCP server, with its opencodex-advertised name. */
|
|
11
39
|
export interface McpToolHandle {
|
|
@@ -44,6 +72,9 @@ export interface CursorMcpManagerOptions {
|
|
|
44
72
|
/** Test seam: provide a transport factory instead of spawning real processes. */
|
|
45
73
|
transportFactory?: (server: ResolvedMcpServer) => Transport;
|
|
46
74
|
log?: (message: string) => void;
|
|
75
|
+
maxTools?: number;
|
|
76
|
+
maxSchemaBytes?: number;
|
|
77
|
+
maxResultBytes?: number;
|
|
47
78
|
}
|
|
48
79
|
|
|
49
80
|
interface ConnectedServer {
|
|
@@ -64,17 +95,25 @@ interface ConnectedServer {
|
|
|
64
95
|
export class CursorMcpManager {
|
|
65
96
|
private readonly connectTimeoutMs: number;
|
|
66
97
|
private readonly callTimeoutMs: number;
|
|
98
|
+
private readonly maxTools: number;
|
|
99
|
+
private readonly maxSchemaBytes: number;
|
|
100
|
+
private readonly maxResultBytes: number;
|
|
67
101
|
private connected?: Promise<void>;
|
|
68
102
|
private readonly servers = new Map<string, ConnectedServer>();
|
|
69
103
|
/** advertisedName -> { serverName, original toolName } */
|
|
70
104
|
private readonly toolIndex = new Map<string, { serverName: string; toolName: string; handle: McpToolHandle }>();
|
|
105
|
+
private toolCatalogBytes = 0;
|
|
71
106
|
|
|
72
107
|
constructor(
|
|
73
108
|
private readonly resolved: ResolvedMcpServer[],
|
|
74
109
|
private readonly options: CursorMcpManagerOptions = {},
|
|
75
110
|
) {
|
|
111
|
+
if (resolved.length > CURSOR_MCP_MAX_SERVERS) throw new McpCatalogLimitError();
|
|
76
112
|
this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
77
113
|
this.callTimeoutMs = options.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
|
|
114
|
+
this.maxTools = options.maxTools ?? CURSOR_MCP_MAX_TOOLS;
|
|
115
|
+
this.maxSchemaBytes = options.maxSchemaBytes ?? CURSOR_MCP_MAX_SCHEMA_BYTES;
|
|
116
|
+
this.maxResultBytes = options.maxResultBytes ?? CURSOR_MCP_MAX_RESULT_BYTES;
|
|
78
117
|
}
|
|
79
118
|
|
|
80
119
|
/** Idempotent, lazy connect + tool discovery across all servers. Never throws. */
|
|
@@ -84,20 +123,45 @@ export class CursorMcpManager {
|
|
|
84
123
|
}
|
|
85
124
|
|
|
86
125
|
private async connectAll(): Promise<void> {
|
|
87
|
-
|
|
126
|
+
const staged: Array<{ connection: ConnectedServer; tools: Array<[string, { serverName: string; toolName: string; handle: McpToolHandle }]>; bytes: number }> = [];
|
|
127
|
+
try {
|
|
128
|
+
for (const server of this.resolved) {
|
|
129
|
+
const connected = await this.connectOne(server);
|
|
130
|
+
if (!connected) continue;
|
|
131
|
+
staged.push(connected);
|
|
132
|
+
const toolCount = staged.reduce((sum, row) => sum + row.tools.length, 0);
|
|
133
|
+
const catalogBytes = staged.reduce((sum, row) => sum + row.bytes, 0);
|
|
134
|
+
if (toolCount > this.maxTools || catalogBytes > CURSOR_MCP_MAX_CATALOG_BYTES) throw new McpCatalogLimitError();
|
|
135
|
+
}
|
|
136
|
+
for (const row of staged) {
|
|
137
|
+
this.servers.set(row.connection.server.serverName, row.connection);
|
|
138
|
+
for (const [name, entry] of row.tools) this.toolIndex.set(name, entry);
|
|
139
|
+
this.toolCatalogBytes += row.bytes;
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
await Promise.all(staged.map(row => row.connection.client.close().catch(() => {})));
|
|
143
|
+
this.servers.clear();
|
|
144
|
+
this.toolIndex.clear();
|
|
145
|
+
this.toolCatalogBytes = 0;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
88
148
|
}
|
|
89
149
|
|
|
90
|
-
private async connectOne(server: ResolvedMcpServer): Promise<
|
|
150
|
+
private async connectOne(server: ResolvedMcpServer): Promise<{ connection: ConnectedServer; tools: Array<[string, { serverName: string; toolName: string; handle: McpToolHandle }]>; bytes: number } | null> {
|
|
151
|
+
let client: Client | undefined;
|
|
91
152
|
try {
|
|
92
153
|
const transport = this.options.transportFactory
|
|
93
154
|
? this.options.transportFactory(server)
|
|
94
155
|
: this.createTransport(server);
|
|
95
|
-
|
|
156
|
+
client = new Client({ name: "opencodex", version: "1.0.0" });
|
|
96
157
|
await this.withTimeout(client.connect(transport), this.connectTimeoutMs, `connect ${server.serverName}`);
|
|
97
|
-
this.
|
|
98
|
-
|
|
158
|
+
const indexed = await this.indexTools(server, client);
|
|
159
|
+
return { connection: { server, client }, tools: indexed.tools, bytes: indexed.bytes };
|
|
99
160
|
} catch (err) {
|
|
161
|
+
if (client) await client.close().catch(() => {});
|
|
100
162
|
this.options.log?.(`[cursor-mcp] server "${server.serverName}" failed to connect: ${errText(err)}`);
|
|
163
|
+
if (err instanceof McpCatalogLimitError || err instanceof McpPayloadTooLargeError) throw err;
|
|
164
|
+
return null;
|
|
101
165
|
}
|
|
102
166
|
}
|
|
103
167
|
|
|
@@ -118,20 +182,31 @@ export class CursorMcpManager {
|
|
|
118
182
|
throw new Error(`MCP server "${server.serverName}" has neither command nor url`);
|
|
119
183
|
}
|
|
120
184
|
|
|
121
|
-
private async indexTools(server: ResolvedMcpServer, client: Client): Promise<
|
|
185
|
+
private async indexTools(server: ResolvedMcpServer, client: Client): Promise<{ tools: Array<[string, { serverName: string; toolName: string; handle: McpToolHandle }]>; bytes: number }> {
|
|
122
186
|
const prefix = server.toolPrefix ?? "";
|
|
123
187
|
const { tools } = await this.withTimeout(client.listTools(), this.connectTimeoutMs, `listTools ${server.serverName}`);
|
|
188
|
+
const additions: Array<[string, { serverName: string; toolName: string; handle: McpToolHandle }]> = [];
|
|
189
|
+
let catalogBytes = 0;
|
|
124
190
|
for (const tool of tools ?? []) {
|
|
191
|
+
if (additions.length >= this.maxTools) throw new McpCatalogLimitError();
|
|
125
192
|
const advertisedName = `${prefix}${tool.name}`;
|
|
193
|
+
const schema = tool.inputSchema ?? {};
|
|
194
|
+
const schemaBytes = utf8.encode(stableJson(schema)).byteLength;
|
|
195
|
+
if (schemaBytes > this.maxSchemaBytes) throw new McpCatalogLimitError();
|
|
196
|
+
catalogBytes += utf8.encode(advertisedName).byteLength
|
|
197
|
+
+ utf8.encode(tool.description ?? "").byteLength
|
|
198
|
+
+ schemaBytes;
|
|
199
|
+
if (catalogBytes > CURSOR_MCP_MAX_CATALOG_BYTES) throw new McpCatalogLimitError();
|
|
126
200
|
const handle: McpToolHandle = {
|
|
127
201
|
serverName: server.serverName,
|
|
128
202
|
toolName: tool.name,
|
|
129
203
|
advertisedName,
|
|
130
204
|
description: tool.description ?? "",
|
|
131
|
-
inputSchema:
|
|
205
|
+
inputSchema: schema,
|
|
132
206
|
};
|
|
133
|
-
|
|
207
|
+
additions.push([advertisedName, { serverName: server.serverName, toolName: tool.name, handle }]);
|
|
134
208
|
}
|
|
209
|
+
return { tools: additions, bytes: catalogBytes };
|
|
135
210
|
}
|
|
136
211
|
|
|
137
212
|
async listToolHandles(): Promise<McpToolHandle[]> {
|
|
@@ -161,6 +236,7 @@ export class CursorMcpManager {
|
|
|
161
236
|
this.callTimeoutMs,
|
|
162
237
|
`callTool ${advertisedName}`,
|
|
163
238
|
);
|
|
239
|
+
if (payloadBytes(result) > this.maxResultBytes) throw new McpPayloadTooLargeError("result");
|
|
164
240
|
return {
|
|
165
241
|
isError: Boolean((result as { isError?: boolean }).isError),
|
|
166
242
|
content: normalizeContent((result as { content?: unknown[] }).content),
|
|
@@ -171,8 +247,14 @@ export class CursorMcpManager {
|
|
|
171
247
|
await this.ensureConnected();
|
|
172
248
|
const targets = server ? [this.servers.get(server)].filter(Boolean) as ConnectedServer[] : [...this.servers.values()];
|
|
173
249
|
const out: McpResourceListing[] = [];
|
|
250
|
+
let catalogBytes = 0;
|
|
174
251
|
for (const conn of targets) {
|
|
175
252
|
const { resources } = await this.withTimeout(conn.client.listResources(), this.callTimeoutMs, `listResources ${conn.server.serverName}`);
|
|
253
|
+
catalogBytes += payloadBytes(resources ?? []);
|
|
254
|
+
if ((resources?.length ?? 0) + out.length > CURSOR_MCP_MAX_RESOURCES
|
|
255
|
+
|| catalogBytes > CURSOR_MCP_MAX_CATALOG_BYTES) {
|
|
256
|
+
throw new McpCatalogLimitError();
|
|
257
|
+
}
|
|
176
258
|
for (const r of resources ?? []) {
|
|
177
259
|
out.push({ uri: r.uri, name: r.name, description: r.description, mimeType: r.mimeType, server: conn.server.serverName });
|
|
178
260
|
}
|
|
@@ -185,8 +267,12 @@ export class CursorMcpManager {
|
|
|
185
267
|
const conn = this.servers.get(server);
|
|
186
268
|
if (!conn) throw new Error(`MCP server not connected: ${server}`);
|
|
187
269
|
const result = await this.withTimeout(conn.client.readResource({ uri }), this.callTimeoutMs, `readResource ${uri}`);
|
|
270
|
+
const rawBytes = payloadBytes(result);
|
|
271
|
+
if (rawBytes > this.maxResultBytes) throw new McpPayloadTooLargeError("resource");
|
|
188
272
|
const first = (result.contents ?? [])[0] as { uri?: string; mimeType?: string; text?: string; blob?: string } | undefined;
|
|
189
273
|
if (!first) return { uri, mimeType: undefined, text: "" };
|
|
274
|
+
const decodedBytes = typeof first.blob === "string" ? decodedBase64Length(first.blob) : 0;
|
|
275
|
+
if (rawBytes + decodedBytes > this.maxResultBytes) throw new McpPayloadTooLargeError("resource");
|
|
190
276
|
return {
|
|
191
277
|
uri: first.uri ?? uri,
|
|
192
278
|
mimeType: first.mimeType,
|
|
@@ -195,10 +281,15 @@ export class CursorMcpManager {
|
|
|
195
281
|
};
|
|
196
282
|
}
|
|
197
283
|
|
|
284
|
+
assertDecodedResultBudget(value: unknown, decodedBytes: number): void {
|
|
285
|
+
if (payloadBytes(value) + decodedBytes > this.maxResultBytes) throw new McpPayloadTooLargeError("result");
|
|
286
|
+
}
|
|
287
|
+
|
|
198
288
|
async dispose(): Promise<void> {
|
|
199
289
|
const conns = [...this.servers.values()];
|
|
200
290
|
this.servers.clear();
|
|
201
291
|
this.toolIndex.clear();
|
|
292
|
+
this.toolCatalogBytes = 0;
|
|
202
293
|
await Promise.all(conns.map(async conn => {
|
|
203
294
|
try {
|
|
204
295
|
await conn.client.close();
|
|
@@ -223,6 +314,12 @@ export class CursorMcpManager {
|
|
|
223
314
|
}
|
|
224
315
|
}
|
|
225
316
|
|
|
317
|
+
export function decodedBase64Length(value: string): number {
|
|
318
|
+
if (!value) return 0;
|
|
319
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
320
|
+
return Math.max(0, Math.floor(value.length * 3 / 4) - padding);
|
|
321
|
+
}
|
|
322
|
+
|
|
226
323
|
function normalizeContent(content: unknown[] | undefined): McpCallResult["content"] {
|
|
227
324
|
if (!Array.isArray(content)) return [];
|
|
228
325
|
return content.map(item => {
|