shortcutxl 0.3.89 → 0.3.91
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/BINARY-INVENTORY.json +13 -13
- package/CHANGELOG.md +7 -1
- package/dist/ai/types.d.ts +2 -0
- package/dist/app/connectors/secret-store.d.ts +8 -0
- package/dist/app/connectors/secret-store.js +73 -1
- package/dist/app/local/tools/execute-code/approval-payload.d.ts +5 -1
- package/dist/app/local/tools/execute-code/executor.d.ts +4 -15
- package/dist/app/local/tools/execute-code/executor.js +56 -146
- package/dist/app/local/tools/execute-code/observability.js +0 -20
- package/dist/app/local/tools/execute-code/tool.js +0 -17
- package/dist/app/local/tools/execute-code/write-analyzer.js +2 -0
- package/dist/app/local/tools/take-screenshot/com-screenshot.js +8 -1
- package/dist/app/local/tools/take-screenshot/mog-screenshot.d.ts +0 -1
- package/dist/app/local/tools/take-screenshot/mog-screenshot.js +10 -17
- package/dist/app/mcp/auth-status.d.ts +2 -0
- package/dist/app/mcp/auth-status.js +5 -1
- package/dist/app/mcp/connection.d.ts +32 -2
- package/dist/app/mcp/connection.js +179 -83
- package/dist/app/mcp/index.js +44 -9
- package/dist/app/mcp/types.d.ts +3 -1
- package/dist/app/modes/ask/prompt.js +1 -1
- package/dist/app/modes/manage/prompt.js +1 -1
- package/dist/app/providers/model-registry.d.ts +1 -0
- package/dist/app/providers/model-registry.js +1 -0
- package/dist/app/providers/model-resolver.js +1 -0
- package/dist/app/session/session-models.js +2 -1
- package/dist/app/tools/take-screenshot.d.ts +3 -0
- package/dist/app/tools/take-screenshot.js +5 -2
- package/dist/cli.js +965 -509
- package/dist/contracts/agent-api.d.ts +4 -4
- package/dist/contracts/agent-api.js +4 -4
- package/dist/contracts/agent-snapshot.d.ts +14 -0
- package/dist/contracts/agent-snapshot.js +6 -0
- package/dist/core/agent-snapshot-builder.js +6 -2
- package/dist/core/prompts/agent-guidelines.d.ts +1 -1
- package/dist/core/prompts/agent-guidelines.js +1 -1
- package/dist/core/prompts/take-screenshot.d.ts +1 -1
- package/dist/core/prompts/take-screenshot.js +1 -0
- package/dist/core/session/compaction/compaction.d.ts +1 -1
- package/dist/core/session/compaction/compaction.js +2 -2
- package/dist/core/session/compaction/constants.d.ts +4 -3
- package/dist/core/session/compaction/constants.js +5 -4
- package/dist/core/session/session-compaction.js +6 -4
- package/dist/embedded-agent/agent-event-funnel.d.ts +0 -2
- package/dist/embedded-agent/agent-event-funnel.js +0 -3
- package/dist/embedded-agent/compaction-wiring.d.ts +2 -2
- package/dist/embedded-agent/compaction-wiring.js +44 -22
- package/dist/embedded-agent/compose.js +6 -5
- package/dist/embedded-agent/host-tools/mcp/contract.d.ts +12 -0
- package/dist/embedded-agent/host-tools/mcp/contract.js +20 -1
- package/dist/embedded-agent/host-tools/mcp/index.d.ts +1 -1
- package/dist/embedded-agent/host-tools/mcp/index.js +1 -1
- package/dist/embedded-agent/host-tools/take-screenshot/contract.d.ts +1 -1
- package/dist/embedded-agent/model-registry.d.ts +2 -0
- package/dist/embedded-agent/model-registry.js +2 -0
- package/dist/embedded-agent/prompt/build.d.ts +0 -1
- package/dist/embedded-agent/prompt/build.js +0 -1
- package/dist/embedded-agent/provider-fast-mode.js +7 -4
- package/dist/embedded-agent/recovery-wiring.d.ts +2 -0
- package/dist/embedded-agent/recovery-wiring.js +1 -13
- package/dist/main.js +34 -3
- package/dist/model-display.js +1 -1
- package/dist/model-ids.d.ts +7 -1
- package/dist/model-ids.js +14 -2
- package/dist/rpc/rpc-host.d.ts +3 -1
- package/dist/rpc/rpc-host.js +4 -1
- package/dist/rpc/rpc-mode.d.ts +1 -1
- package/dist/rpc/rpc-types.d.ts +11 -0
- package/dist/shortcut-model-catalog.d.ts +2 -1
- package/dist/shortcut-model-catalog.js +49 -10
- package/dist/subagent-model-policy.js +1 -0
- package/dist/utils/api-reference.d.ts +0 -2
- package/dist/utils/api-reference.js +21 -5
- package/dist/utils/mog-headless-api-reference.js +0 -1
- package/native-app/main/index.js +2 -2
- package/native-app/package.json +1 -1
- package/native-app/renderer/assets/{index-SuRESuDS.js → index-B4oy3UFe.js} +90 -24
- package/native-app/renderer/index.html +1 -1
- package/package.json +6 -1
- package/skills/mcp/SKILL.md +2 -2
- package/user-docs/dist/index.html +14 -26
- package/user-docs/security.md +4 -9
- package/xll/modules/shortcut_xl/_exec_entry.py +19 -4
- package/xll/modules/shortcut_xl/_exec_sandbox.py +2 -2
- package/xll/modules/shortcut_xl/_log.py +9 -2
- package/xll/modules/shortcut_xl/_threading.py +1 -1
- package/xll/python/Lib/site-packages/httpx-0.28.1.dist-info/RECORD +1 -1
- package/xll/python/Lib/site-packages/idna-3.18.dist-info/RECORD +1 -1
- package/xll/python/Lib/site-packages/pip-26.2.dist-info/RECORD +3 -3
- package/xll/python/Lib/site-packages/pywin32-311.dist-info/RECORD +2 -2
- package/xll/python/Scripts/httpx.exe +0 -0
- package/xll/python/Scripts/idna.exe +0 -0
- package/xll/python/Scripts/pip.exe +0 -0
- package/xll/python/Scripts/pip3.13.exe +0 -0
- package/xll/python/Scripts/pip3.exe +0 -0
- package/xll/python/Scripts/pywin32_postinstall.exe +0 -0
- package/xll/python/Scripts/pywin32_testall.exe +0 -0
|
@@ -13,6 +13,7 @@ async function captureComScreenshot(host, range, signal) {
|
|
|
13
13
|
headers: { 'Content-Type': 'application/json' },
|
|
14
14
|
body: JSON.stringify({
|
|
15
15
|
control: 'screenshot',
|
|
16
|
+
workbook: range.workbook,
|
|
16
17
|
sheet: range.sheet,
|
|
17
18
|
range: range.cell_range
|
|
18
19
|
}),
|
|
@@ -43,6 +44,7 @@ export async function captureComScreenshots(ranges, signal, resolveTarget) {
|
|
|
43
44
|
const msg = error instanceof Error ? error.message : String(error);
|
|
44
45
|
console.error(`[take_screenshot] COM capture unavailable: ${msg}`);
|
|
45
46
|
return ranges.map((range) => ({
|
|
47
|
+
workbook: range.workbook,
|
|
46
48
|
sheet: range.sheet,
|
|
47
49
|
cell_range: range.cell_range,
|
|
48
50
|
data: null
|
|
@@ -58,7 +60,12 @@ export async function captureComScreenshots(ranges, signal, resolveTarget) {
|
|
|
58
60
|
const msg = error instanceof Error ? error.message : String(error);
|
|
59
61
|
console.error(`[take_screenshot] COM capture failed for ${range.sheet}!${range.cell_range}: ${msg}`);
|
|
60
62
|
}
|
|
61
|
-
screenshots.push({
|
|
63
|
+
screenshots.push({
|
|
64
|
+
workbook: range.workbook,
|
|
65
|
+
sheet: range.sheet,
|
|
66
|
+
cell_range: range.cell_range,
|
|
67
|
+
data
|
|
68
|
+
});
|
|
62
69
|
}
|
|
63
70
|
return screenshots;
|
|
64
71
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Screenshot, ScreenshotRange } from '../../../tools/take-screenshot.js';
|
|
2
2
|
interface MogSheetPoolWithScreenshots {
|
|
3
|
-
activeWorkbookName: string | null | undefined;
|
|
4
3
|
getWorkbook(name: string): unknown;
|
|
5
4
|
}
|
|
6
5
|
export declare function captureMogScreenshots(pool: MogSheetPoolWithScreenshots, ranges: ScreenshotRange[], _signal?: AbortSignal): Promise<Screenshot[]>;
|
|
@@ -4,26 +4,14 @@ function isMogWorkbookWithScreenshots(value) {
|
|
|
4
4
|
typeof value.captureScreenshot === 'function');
|
|
5
5
|
}
|
|
6
6
|
export async function captureMogScreenshots(pool, ranges, _signal) {
|
|
7
|
-
if (!pool.activeWorkbookName) {
|
|
8
|
-
return ranges.map((range) => ({
|
|
9
|
-
sheet: range.sheet,
|
|
10
|
-
cell_range: range.cell_range,
|
|
11
|
-
data: null
|
|
12
|
-
}));
|
|
13
|
-
}
|
|
14
|
-
const workbook = pool.getWorkbook(pool.activeWorkbookName);
|
|
15
|
-
if (!isMogWorkbookWithScreenshots(workbook)) {
|
|
16
|
-
console.error('[take_screenshot] Active Mog workbook does not support screenshot capture');
|
|
17
|
-
return ranges.map((range) => ({
|
|
18
|
-
sheet: range.sheet,
|
|
19
|
-
cell_range: range.cell_range,
|
|
20
|
-
data: null
|
|
21
|
-
}));
|
|
22
|
-
}
|
|
23
7
|
const screenshots = [];
|
|
24
8
|
for (const range of ranges) {
|
|
25
9
|
let data = null;
|
|
26
10
|
try {
|
|
11
|
+
const workbook = pool.getWorkbook(range.workbook);
|
|
12
|
+
if (!isMogWorkbookWithScreenshots(workbook)) {
|
|
13
|
+
throw new Error(`Workbook does not support screenshot capture: ${range.workbook}`);
|
|
14
|
+
}
|
|
27
15
|
const pngBuffer = await workbook.captureScreenshot(range.sheet, range.cell_range);
|
|
28
16
|
data = Buffer.from(pngBuffer).toString('base64');
|
|
29
17
|
}
|
|
@@ -31,7 +19,12 @@ export async function captureMogScreenshots(pool, ranges, _signal) {
|
|
|
31
19
|
const msg = error instanceof Error ? error.message : String(error);
|
|
32
20
|
console.error(`[take_screenshot] Mog capture failed for ${range.sheet}!${range.cell_range}: ${msg}`);
|
|
33
21
|
}
|
|
34
|
-
screenshots.push({
|
|
22
|
+
screenshots.push({
|
|
23
|
+
workbook: range.workbook,
|
|
24
|
+
sheet: range.sheet,
|
|
25
|
+
cell_range: range.cell_range,
|
|
26
|
+
data
|
|
27
|
+
});
|
|
35
28
|
}
|
|
36
29
|
return screenshots;
|
|
37
30
|
}
|
|
@@ -6,6 +6,8 @@ export interface ResolveMcpAuthStatusOptions {
|
|
|
6
6
|
authStorage?: AuthStorage;
|
|
7
7
|
mcpOAuthStore?: McpOAuthStore;
|
|
8
8
|
discovery?: McpAuthDiscoveryMode;
|
|
9
|
+
/** Pre-fetched connection secrets by name; when absent the keychain is read directly. */
|
|
10
|
+
connectionSecrets?: ReadonlyMap<string, Record<string, string> | undefined>;
|
|
9
11
|
}
|
|
10
12
|
export declare function resolveMcpAuthStatus(serverName: string, config: McpServerConfig, options?: ResolveMcpAuthStatusOptions): Promise<McpAuthStatus>;
|
|
11
13
|
export declare function getMcpAuthStatus(serverName: string, config: McpServerConfig, authStorage?: AuthStorage, mcpOAuthStore?: McpOAuthStore): Promise<McpAuthStatus>;
|
|
@@ -10,7 +10,11 @@ export async function resolveMcpAuthStatus(serverName, config, options = {}) {
|
|
|
10
10
|
return 'unsupported';
|
|
11
11
|
}
|
|
12
12
|
const staticHeaders = { ...(config.headers ?? {}) };
|
|
13
|
-
const secretHeaders = config.connection
|
|
13
|
+
const secretHeaders = !config.connection
|
|
14
|
+
? undefined
|
|
15
|
+
: options.connectionSecrets
|
|
16
|
+
? options.connectionSecrets.get(config.connection)
|
|
17
|
+
: getSecret(config.connection);
|
|
14
18
|
if (hasConfiguredHeaders(staticHeaders) || hasConfiguredHeaders(secretHeaders)) {
|
|
15
19
|
return 'configured';
|
|
16
20
|
}
|
|
@@ -4,17 +4,47 @@
|
|
|
4
4
|
import type { AuthStorage } from '../auth/auth-storage.js';
|
|
5
5
|
import type { McpOAuthStore } from './oauth-state-store.js';
|
|
6
6
|
import { type McpConnection, type McpFailureKind, type McpRequestPolicy, type McpServerConfig } from './types.js';
|
|
7
|
+
/** Abort signal plus absolute deadline that bounds one server's whole startup. */
|
|
8
|
+
export interface McpStartupBudget {
|
|
9
|
+
signal: AbortSignal;
|
|
10
|
+
deadlineAt: number;
|
|
11
|
+
}
|
|
12
|
+
export interface McpConnectOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Connection secrets already read from the keychain, keyed by connection
|
|
15
|
+
* name. When absent, the server reads its own secret synchronously.
|
|
16
|
+
*/
|
|
17
|
+
connectionSecrets?: ReadonlyMap<string, Record<string, string> | undefined>;
|
|
18
|
+
/** Budget shared across the whole startup. Direct callers get one from the config's startup timeout. */
|
|
19
|
+
budget?: McpStartupBudget;
|
|
20
|
+
}
|
|
7
21
|
export declare class McpConnectionError extends Error {
|
|
8
22
|
readonly failureKind: McpFailureKind;
|
|
9
23
|
constructor(failureKind: McpFailureKind, message: string);
|
|
10
24
|
}
|
|
11
25
|
export declare function resolveStartupTimeoutMs(config: McpServerConfig): number;
|
|
12
26
|
export declare function buildRequestPolicy(config: McpServerConfig): McpRequestPolicy;
|
|
27
|
+
/**
|
|
28
|
+
* Run one server's startup under a single deadline. The budget's signal is
|
|
29
|
+
* threaded into every SDK request so a stalled initialize or tools/list is
|
|
30
|
+
* cancelled, and the race guarantees the deadline even for steps that cannot
|
|
31
|
+
* observe the signal (process spawn, keychain reads).
|
|
32
|
+
*/
|
|
33
|
+
export declare function withStartupBudget<T>(name: string, timeoutMs: number, operation: (budget: McpStartupBudget) => Promise<T>): Promise<T>;
|
|
34
|
+
/** SDK request options for initialize and tools/list under a startup budget. */
|
|
35
|
+
export declare function requestOptions(budget: McpStartupBudget): {
|
|
36
|
+
signal: AbortSignal;
|
|
37
|
+
timeout: number;
|
|
38
|
+
};
|
|
39
|
+
/** Whether a failed hosted startup is safe to repeat with a fresh MCP session. */
|
|
40
|
+
export declare function isTransientHostedStartupError(error: unknown): boolean;
|
|
13
41
|
export declare function createHostedMcpLoggingFetch(serverName: string, fetchImpl?: typeof fetch): typeof fetch;
|
|
14
42
|
/**
|
|
15
|
-
* Connect a server, list tools, and return a live MCP connection.
|
|
43
|
+
* Connect a server, list tools, and return a live MCP connection. The whole
|
|
44
|
+
* sequence runs under `options.budget`, or under a fresh budget from the
|
|
45
|
+
* config's startup timeout when the caller did not supply one.
|
|
16
46
|
*/
|
|
17
|
-
export declare function connectServer(name: string, config: McpServerConfig, defaultCwd: string, authStorage?: AuthStorage, mcpOAuthStore?: McpOAuthStore): Promise<McpConnection>;
|
|
47
|
+
export declare function connectServer(name: string, config: McpServerConfig, defaultCwd: string, authStorage?: AuthStorage, mcpOAuthStore?: McpOAuthStore, options?: McpConnectOptions): Promise<McpConnection>;
|
|
18
48
|
/**
|
|
19
49
|
* Call a tool on a connected server. Returns the result text or throws.
|
|
20
50
|
*/
|
|
@@ -10,16 +10,40 @@ import { mkdir, writeFile } from 'fs/promises';
|
|
|
10
10
|
import { basename, extname, join } from 'path';
|
|
11
11
|
import { getAgentTempDir } from '../../config.js';
|
|
12
12
|
import { log } from '../../shared/logging/agent-log.js';
|
|
13
|
+
import { sleep } from '../../utils/sleep.js';
|
|
13
14
|
import { getSecret } from '../connectors/secret-store.js';
|
|
14
15
|
import { getHostedMcpOAuthProviderId } from './hosted-oauth-id.js';
|
|
15
16
|
import { createHostedMcpRuntimeAuthProvider } from './hosted-oauth-provider.js';
|
|
16
17
|
import { resolveHostedMcpOAuthTimeoutMs, supportsHostedMcpOAuth } from './oauth-discovery.js';
|
|
17
18
|
import { withHostedMcpProxyFetchOptions } from './proxy-fetch.js';
|
|
18
19
|
import { isHostedServerConfig } from './types.js';
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Default budget for one server's whole startup: credential lookup, connect and
|
|
22
|
+
* initialize, OAuth refresh, and tool discovery. Managed Windows fleets behind
|
|
23
|
+
* proxies and EDR routinely take 60-90s here, so the budget is generous while
|
|
24
|
+
* still sitting under the 180s RPC readiness budget in `rpc-client.ts`.
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_STARTUP_TIMEOUT_MS = 120_000;
|
|
20
27
|
const DEFAULT_TOOL_CALL_TIMEOUT_MS = 600_000;
|
|
21
28
|
const DEFAULT_RESET_TIMEOUT_ON_PROGRESS = true;
|
|
22
29
|
const MCP_OUTPUT_DIR_NAME = 'mcp';
|
|
30
|
+
const HOSTED_STARTUP_RETRY_DELAY_MS = 250;
|
|
31
|
+
const TRANSIENT_NETWORK_ERROR_CODES = new Set([
|
|
32
|
+
'EAI_AGAIN',
|
|
33
|
+
'ECONNABORTED',
|
|
34
|
+
'ECONNREFUSED',
|
|
35
|
+
'ECONNRESET',
|
|
36
|
+
'EHOSTUNREACH',
|
|
37
|
+
'ENETUNREACH',
|
|
38
|
+
'ENOTFOUND',
|
|
39
|
+
'EPIPE',
|
|
40
|
+
'ETIMEDOUT',
|
|
41
|
+
'UND_ERR_BODY_TIMEOUT',
|
|
42
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
43
|
+
'UND_ERR_HEADERS_TIMEOUT',
|
|
44
|
+
'UND_ERR_SOCKET'
|
|
45
|
+
]);
|
|
46
|
+
const TRANSIENT_HTTP_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
23
47
|
function isRecord(value) {
|
|
24
48
|
return typeof value === 'object' && value !== null;
|
|
25
49
|
}
|
|
@@ -231,14 +255,68 @@ export function buildRequestPolicy(config) {
|
|
|
231
255
|
: {})
|
|
232
256
|
};
|
|
233
257
|
}
|
|
234
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Run one server's startup under a single deadline. The budget's signal is
|
|
260
|
+
* threaded into every SDK request so a stalled initialize or tools/list is
|
|
261
|
+
* cancelled, and the race guarantees the deadline even for steps that cannot
|
|
262
|
+
* observe the signal (process spawn, keychain reads).
|
|
263
|
+
*/
|
|
264
|
+
export async function withStartupBudget(name, timeoutMs, operation) {
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
const timeoutError = new McpConnectionError('startup_timeout', `MCP server '${name}' did not finish connection and tool discovery within ${timeoutMs / 1000}s.`);
|
|
267
|
+
const expired = new Promise((_, reject) => {
|
|
268
|
+
controller.signal.addEventListener('abort', () => reject(timeoutError), { once: true });
|
|
269
|
+
});
|
|
270
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
271
|
+
if (typeof timer === 'object' && 'unref' in timer)
|
|
272
|
+
timer.unref();
|
|
273
|
+
try {
|
|
274
|
+
return await Promise.race([
|
|
275
|
+
operation({ signal: controller.signal, deadlineAt: Date.now() + timeoutMs }),
|
|
276
|
+
expired
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
// The SDK surfaces our abort as its own error; report the budget instead.
|
|
281
|
+
throw controller.signal.aborted ? timeoutError : error;
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
clearTimeout(timer);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The SDK arms its own per-request timer (60s when none is given), so it cannot
|
|
289
|
+
* be the budget's timekeeper: two timers set to the same instant race, and the
|
|
290
|
+
* SDK's raw "Request timed out" would sometimes win. The budget's abort signal
|
|
291
|
+
* is the single source of truth; the SDK timeout only sits above it so the
|
|
292
|
+
* default 60s never cuts a longer budget short.
|
|
293
|
+
*/
|
|
294
|
+
const SDK_TIMEOUT_SLACK_MS = 1_000;
|
|
295
|
+
/** SDK request options for initialize and tools/list under a startup budget. */
|
|
296
|
+
export function requestOptions(budget) {
|
|
297
|
+
return {
|
|
298
|
+
signal: budget.signal,
|
|
299
|
+
timeout: Math.max(1, budget.deadlineAt - Date.now()) + SDK_TIMEOUT_SLACK_MS
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** Secret for `config.connection`, from the pre-fetched map when the manager supplied one. */
|
|
303
|
+
function resolveConnectionSecret(name, connection, options) {
|
|
304
|
+
const secret = options.connectionSecrets
|
|
305
|
+
? options.connectionSecrets.get(connection)
|
|
306
|
+
: getSecret(connection);
|
|
307
|
+
if (!secret) {
|
|
308
|
+
throw new Error(`MCP server '${name}' references connection '${connection}' but no credentials found in keychain. Run /connect to set it up.`);
|
|
309
|
+
}
|
|
310
|
+
return secret;
|
|
311
|
+
}
|
|
312
|
+
/** Best-effort teardown after a failed startup; a stdio close waits on the child, so never block on it. */
|
|
313
|
+
function discardClient(client) {
|
|
314
|
+
void client.close().catch(() => { });
|
|
315
|
+
}
|
|
316
|
+
function buildHostedHeaders(name, config, options) {
|
|
235
317
|
const headers = { ...(config.headers ?? {}) };
|
|
236
318
|
if (config.connection) {
|
|
237
|
-
|
|
238
|
-
if (!secrets) {
|
|
239
|
-
throw new Error(`MCP server '${name}' references connection '${config.connection}' but no credentials found in keychain. Run /connect to set it up.`);
|
|
240
|
-
}
|
|
241
|
-
Object.assign(headers, secrets);
|
|
319
|
+
Object.assign(headers, resolveConnectionSecret(name, config.connection, options));
|
|
242
320
|
}
|
|
243
321
|
return headers;
|
|
244
322
|
}
|
|
@@ -354,6 +432,32 @@ function summarizeErrorForLog(error, depth = 0) {
|
|
|
354
432
|
}
|
|
355
433
|
return details;
|
|
356
434
|
}
|
|
435
|
+
/** Whether a failed hosted startup is safe to repeat with a fresh MCP session. */
|
|
436
|
+
export function isTransientHostedStartupError(error) {
|
|
437
|
+
if (error instanceof UnauthorizedError || error instanceof McpConnectionError) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
if (error instanceof StreamableHTTPError &&
|
|
441
|
+
typeof error.code === 'number' &&
|
|
442
|
+
TRANSIENT_HTTP_STATUS_CODES.has(error.code)) {
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
let current = error;
|
|
446
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
447
|
+
if (current === null || (typeof current !== 'object' && typeof current !== 'function')) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const record = current;
|
|
451
|
+
if (record.name === 'AbortError')
|
|
452
|
+
return false;
|
|
453
|
+
if (typeof record.code === 'string' &&
|
|
454
|
+
TRANSIENT_NETWORK_ERROR_CODES.has(record.code.toUpperCase())) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
current = record.cause;
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
357
461
|
async function readResponseSnippet(response) {
|
|
358
462
|
try {
|
|
359
463
|
const text = await response.clone().text();
|
|
@@ -383,10 +487,33 @@ export function createHostedMcpLoggingFetch(serverName, fetchImpl = fetch) {
|
|
|
383
487
|
return response;
|
|
384
488
|
};
|
|
385
489
|
}
|
|
386
|
-
|
|
387
|
-
|
|
490
|
+
/** Discover the server's tools inside the budget, releasing the client if discovery fails. */
|
|
491
|
+
async function discoverTools(name, client, budget) {
|
|
492
|
+
let toolsResult;
|
|
493
|
+
try {
|
|
494
|
+
budget.signal.throwIfAborted();
|
|
495
|
+
toolsResult = await client.listTools(undefined, requestOptions(budget));
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
discardClient(client);
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
const tools = (toolsResult.tools ?? []).map((t) => ({
|
|
502
|
+
name: t.name,
|
|
503
|
+
description: t.description,
|
|
504
|
+
parameterNames: t.inputSchema?.properties
|
|
505
|
+
? Object.keys(t.inputSchema.properties)
|
|
506
|
+
: [],
|
|
507
|
+
inputSchema: (t.inputSchema ?? {})
|
|
508
|
+
}));
|
|
509
|
+
if (tools.length === 0) {
|
|
510
|
+
log.info('mcp', `MCP server '${name}' connected but exposed 0 tools.`);
|
|
511
|
+
}
|
|
512
|
+
return tools;
|
|
513
|
+
}
|
|
514
|
+
async function connectHostedServerAttempt(name, config, authStorage, mcpOAuthStore, options, budget, retriedAfterOAuthRefresh = false) {
|
|
388
515
|
const requestPolicy = buildRequestPolicy(config);
|
|
389
|
-
const headers =
|
|
516
|
+
const headers = buildHostedHeaders(name, config, options);
|
|
390
517
|
const authProvider = authStorage &&
|
|
391
518
|
mcpOAuthStore &&
|
|
392
519
|
!hasAuthorizationHeader(headers) &&
|
|
@@ -399,25 +526,12 @@ async function connectHostedServer(name, config, authStorage, mcpOAuthStore, ret
|
|
|
399
526
|
authProvider
|
|
400
527
|
});
|
|
401
528
|
const client = new Client({ name: 'shortcut-agent', version: '1.0.0' }, { capabilities: {} });
|
|
402
|
-
let timer;
|
|
403
|
-
const connectPromise = client.connect(transport);
|
|
404
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
405
|
-
timer = setTimeout(() => reject(new Error(`did not respond within ${startupTimeoutMs / 1000}s`)), startupTimeoutMs);
|
|
406
|
-
if (typeof timer === 'object' && 'unref' in timer)
|
|
407
|
-
timer.unref();
|
|
408
|
-
});
|
|
409
529
|
try {
|
|
410
|
-
await
|
|
411
|
-
if (timer)
|
|
412
|
-
clearTimeout(timer);
|
|
530
|
+
await client.connect(transport, requestOptions(budget));
|
|
413
531
|
}
|
|
414
532
|
catch (err) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
catch {
|
|
419
|
-
// Ignore close errors during cleanup.
|
|
420
|
-
}
|
|
533
|
+
discardClient(client);
|
|
534
|
+
budget.signal.throwIfAborted();
|
|
421
535
|
if (err instanceof UnauthorizedError ||
|
|
422
536
|
(err instanceof StreamableHTTPError && err.code === 401)) {
|
|
423
537
|
const providerId = getHostedMcpOAuthProviderId(name, config);
|
|
@@ -426,12 +540,13 @@ async function connectHostedServer(name, config, authStorage, mcpOAuthStore, ret
|
|
|
426
540
|
!retriedAfterOAuthRefresh &&
|
|
427
541
|
authStorage.get(providerId)?.type === 'oauth') {
|
|
428
542
|
const refreshedToken = await authStorage.getApiKey(providerId, { forceRefresh: true });
|
|
543
|
+
budget.signal.throwIfAborted();
|
|
429
544
|
if (refreshedToken) {
|
|
430
|
-
return
|
|
545
|
+
return connectHostedServerAttempt(name, config, authStorage, mcpOAuthStore, options, budget, true);
|
|
431
546
|
}
|
|
432
547
|
}
|
|
433
548
|
const supportsOAuth = await supportsHostedMcpOAuth(name, config, mcpOAuthStore, {
|
|
434
|
-
timeoutMs: resolveHostedMcpOAuthTimeoutMs(config)
|
|
549
|
+
timeoutMs: Math.min(resolveHostedMcpOAuthTimeoutMs(config), requestOptions(budget).timeout)
|
|
435
550
|
}).catch(() => !!config.oauth);
|
|
436
551
|
const providerName = config.oauth?.providerName ?? `MCP: ${name}`;
|
|
437
552
|
throw new McpConnectionError('auth_required', supportsOAuth
|
|
@@ -440,31 +555,32 @@ async function connectHostedServer(name, config, authStorage, mcpOAuthStore, ret
|
|
|
440
555
|
}
|
|
441
556
|
throw err;
|
|
442
557
|
}
|
|
443
|
-
const
|
|
444
|
-
const tools = (toolsResult.tools ?? []).map((t) => ({
|
|
445
|
-
name: t.name,
|
|
446
|
-
description: t.description,
|
|
447
|
-
parameterNames: t.inputSchema?.properties
|
|
448
|
-
? Object.keys(t.inputSchema.properties)
|
|
449
|
-
: [],
|
|
450
|
-
inputSchema: (t.inputSchema ?? {})
|
|
451
|
-
}));
|
|
452
|
-
if (tools.length === 0) {
|
|
453
|
-
log.info('mcp', `Hosted MCP server '${name}' connected but exposed 0 tools.`);
|
|
454
|
-
}
|
|
558
|
+
const tools = await discoverTools(name, client, budget);
|
|
455
559
|
return { name, client, transport, tools, requestPolicy };
|
|
456
560
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
561
|
+
/** Retry transient hosted startup failures with a fresh client and transport each time. */
|
|
562
|
+
async function connectHostedServer(name, config, authStorage, mcpOAuthStore, options, budget) {
|
|
563
|
+
try {
|
|
564
|
+
return await connectHostedServerAttempt(name, config, authStorage, mcpOAuthStore, options, budget);
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
budget.signal.throwIfAborted();
|
|
568
|
+
if (!isTransientHostedStartupError(error) ||
|
|
569
|
+
Date.now() + HOSTED_STARTUP_RETRY_DELAY_MS >= budget.deadlineAt) {
|
|
570
|
+
throw error;
|
|
465
571
|
}
|
|
466
|
-
|
|
572
|
+
log.warn('mcp', `MCP server '${name}' had a transient startup failure; retrying once with a fresh session. error=${JSON.stringify(summarizeErrorForLog(error))}`);
|
|
573
|
+
await sleep(HOSTED_STARTUP_RETRY_DELAY_MS, budget.signal);
|
|
574
|
+
return connectHostedServerAttempt(name, config, authStorage, mcpOAuthStore, options, budget);
|
|
467
575
|
}
|
|
576
|
+
}
|
|
577
|
+
async function connectStdioServer(name, config, defaultCwd, options, budget) {
|
|
578
|
+
const requestPolicy = buildRequestPolicy(config);
|
|
579
|
+
const serverEnv = {
|
|
580
|
+
...process.env,
|
|
581
|
+
...(config.env ?? {}),
|
|
582
|
+
...(config.connection ? resolveConnectionSecret(name, config.connection, options) : {})
|
|
583
|
+
};
|
|
468
584
|
const transport = new StdioClientTransport({
|
|
469
585
|
command: config.command,
|
|
470
586
|
args: config.args,
|
|
@@ -480,52 +596,32 @@ async function connectStdioServer(name, config, defaultCwd) {
|
|
|
480
596
|
}
|
|
481
597
|
});
|
|
482
598
|
const client = new Client({ name: 'shortcut-agent', version: '1.0.0' }, { capabilities: {} });
|
|
483
|
-
const connectPromise = client.connect(transport);
|
|
484
|
-
let timer;
|
|
485
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
486
|
-
timer = setTimeout(() => reject(new Error(`did not respond within ${startupTimeoutMs / 1000}s`)), startupTimeoutMs);
|
|
487
|
-
if (typeof timer === 'object' && 'unref' in timer)
|
|
488
|
-
timer.unref();
|
|
489
|
-
});
|
|
490
599
|
try {
|
|
491
|
-
await
|
|
492
|
-
if (timer)
|
|
493
|
-
clearTimeout(timer);
|
|
600
|
+
await client.connect(transport, requestOptions(budget));
|
|
494
601
|
}
|
|
495
602
|
catch (err) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
catch {
|
|
500
|
-
// Ignore close errors during cleanup.
|
|
501
|
-
}
|
|
603
|
+
discardClient(client);
|
|
604
|
+
budget.signal.throwIfAborted();
|
|
502
605
|
const stderr = stderrBuf.trim();
|
|
503
606
|
const base = err instanceof Error ? err.message : String(err);
|
|
504
607
|
const detail = stderr ? `${base}\nServer stderr: ${stderr}` : base;
|
|
505
608
|
throw new McpConnectionError('connect_failed', detail);
|
|
506
609
|
}
|
|
507
|
-
const
|
|
508
|
-
const tools = (toolsResult.tools ?? []).map((t) => ({
|
|
509
|
-
name: t.name,
|
|
510
|
-
description: t.description,
|
|
511
|
-
parameterNames: t.inputSchema?.properties
|
|
512
|
-
? Object.keys(t.inputSchema.properties)
|
|
513
|
-
: [],
|
|
514
|
-
inputSchema: (t.inputSchema ?? {})
|
|
515
|
-
}));
|
|
516
|
-
if (tools.length === 0) {
|
|
517
|
-
log.info('mcp', `MCP server '${name}' connected but exposed 0 tools.`);
|
|
518
|
-
}
|
|
610
|
+
const tools = await discoverTools(name, client, budget);
|
|
519
611
|
return { name, client, transport, tools, requestPolicy };
|
|
520
612
|
}
|
|
521
613
|
/**
|
|
522
|
-
* Connect a server, list tools, and return a live MCP connection.
|
|
614
|
+
* Connect a server, list tools, and return a live MCP connection. The whole
|
|
615
|
+
* sequence runs under `options.budget`, or under a fresh budget from the
|
|
616
|
+
* config's startup timeout when the caller did not supply one.
|
|
523
617
|
*/
|
|
524
|
-
export async function connectServer(name, config, defaultCwd, authStorage, mcpOAuthStore) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
return
|
|
618
|
+
export async function connectServer(name, config, defaultCwd, authStorage, mcpOAuthStore, options = {}) {
|
|
619
|
+
const connect = (budget) => isHostedServerConfig(config)
|
|
620
|
+
? connectHostedServer(name, config, authStorage, mcpOAuthStore, options, budget)
|
|
621
|
+
: connectStdioServer(name, config, defaultCwd, options, budget);
|
|
622
|
+
return options.budget
|
|
623
|
+
? connect(options.budget)
|
|
624
|
+
: withStartupBudget(name, resolveStartupTimeoutMs(config), connect);
|
|
529
625
|
}
|
|
530
626
|
/**
|
|
531
627
|
* Call a tool on a connected server. Returns the result text or throws.
|
package/dist/app/mcp/index.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* MCP client manager — composition root for the MCP subsystem.
|
|
3
3
|
*/
|
|
4
4
|
import { log } from '../../shared/logging/agent-log.js';
|
|
5
|
+
import { getSecrets } from '../connectors/secret-store.js';
|
|
5
6
|
import { resolveMcpAuthStatus } from './auth-status.js';
|
|
6
7
|
import { buildCatalog, buildStatusLine } from './catalog.js';
|
|
7
8
|
import { loadMcpConfig } from './config.js';
|
|
8
|
-
import { closeConnection, connectServer, McpConnectionError } from './connection.js';
|
|
9
|
+
import { closeConnection, connectServer, McpConnectionError, resolveStartupTimeoutMs, withStartupBudget } from './connection.js';
|
|
9
10
|
import { getHostedMcpOAuthProviderId } from './hosted-oauth-id.js';
|
|
10
11
|
import { registerHostedMcpOAuthProviders, unregisterHostedMcpOAuthProviders } from './hosted-oauth-provider.js';
|
|
11
12
|
import { createMcpDescribeTool } from './mcp-describe-tool.js';
|
|
@@ -20,6 +21,25 @@ function resolveDisplayedAuthStatus(connection, rawAuthStatus, failureKind) {
|
|
|
20
21
|
}
|
|
21
22
|
return rawAuthStatus;
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Read every referenced connection secret in one asynchronous batch so that a
|
|
26
|
+
* slow keychain (PowerShell on managed Windows) neither blocks the event loop
|
|
27
|
+
* nor runs once per server. A failed batch is reported per server as a
|
|
28
|
+
* connection failure instead of masquerading as "no credentials".
|
|
29
|
+
*/
|
|
30
|
+
async function loadConnectionSecrets(servers, timeoutMs) {
|
|
31
|
+
const names = [
|
|
32
|
+
...new Set(Object.values(servers)
|
|
33
|
+
.map((config) => config.connection)
|
|
34
|
+
.filter((name) => typeof name === 'string'))
|
|
35
|
+
];
|
|
36
|
+
try {
|
|
37
|
+
return await getSecrets(names, timeoutMs);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new McpConnectionError('connect_failed', `Could not read connection credentials from the keychain: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
23
43
|
export async function createMcpManager(options) {
|
|
24
44
|
const servers = loadMcpConfig({
|
|
25
45
|
cwd: options.cwd,
|
|
@@ -35,14 +55,28 @@ export async function createMcpManager(options) {
|
|
|
35
55
|
const failureKinds = new Map();
|
|
36
56
|
const authStatuses = new Map();
|
|
37
57
|
let shutDown = false;
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
const startupTimeouts = serverNames.map((name) => resolveStartupTimeoutMs(servers[name]));
|
|
59
|
+
const connectionSecrets = loadConnectionSecrets(servers, Math.max(...startupTimeouts));
|
|
60
|
+
const startupMs = new Map();
|
|
61
|
+
const results = await Promise.allSettled(serverNames.map(async (name, index) => {
|
|
62
|
+
const config = servers[name];
|
|
63
|
+
const startedAt = Date.now();
|
|
64
|
+
try {
|
|
65
|
+
return await withStartupBudget(name, startupTimeouts[index], async (budget) => {
|
|
66
|
+
const secrets = config.connection ? await connectionSecrets : undefined;
|
|
67
|
+
authStatuses.set(name, await resolveMcpAuthStatus(name, config, {
|
|
68
|
+
authStorage: options.authStorage,
|
|
69
|
+
mcpOAuthStore: options.mcpOAuthStore,
|
|
70
|
+
discovery: 'cached',
|
|
71
|
+
connectionSecrets: secrets
|
|
72
|
+
}));
|
|
73
|
+
const conn = await connectServer(name, config, options.cwd, options.authStorage, options.mcpOAuthStore, { connectionSecrets: secrets, budget });
|
|
74
|
+
return { name, conn };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
startupMs.set(name, Date.now() - startedAt);
|
|
79
|
+
}
|
|
46
80
|
}));
|
|
47
81
|
for (const [idx, result] of results.entries()) {
|
|
48
82
|
if (result.status === 'fulfilled') {
|
|
@@ -113,6 +147,7 @@ export async function createMcpManager(options) {
|
|
|
113
147
|
: {}),
|
|
114
148
|
toolCount: connection?.tools.length ?? 0,
|
|
115
149
|
toolNames: connection?.tools.map((t) => t.name) ?? [],
|
|
150
|
+
startupMs: startupMs.get(name) ?? 0,
|
|
116
151
|
...(failure ? { error: failure } : {})
|
|
117
152
|
};
|
|
118
153
|
});
|
package/dist/app/mcp/types.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export interface McpHostedServerConfig {
|
|
|
67
67
|
}
|
|
68
68
|
export type McpServerConfig = McpStdioServerConfig | McpHostedServerConfig;
|
|
69
69
|
export type McpAuthStatus = 'unsupported' | 'not_logged_in' | 'configured' | 'oauth';
|
|
70
|
-
export type McpFailureKind = 'auth_required' | 'connect_failed';
|
|
70
|
+
export type McpFailureKind = 'auth_required' | 'connect_failed' | 'startup_timeout';
|
|
71
71
|
export declare function isHostedServerConfig(config: McpServerConfig): config is McpHostedServerConfig;
|
|
72
72
|
export declare function isStdioServerConfig(config: McpServerConfig): config is McpStdioServerConfig;
|
|
73
73
|
export type McpTransport = StdioClientTransport | StreamableHTTPClientTransport;
|
|
@@ -100,6 +100,8 @@ export interface McpServerStatus {
|
|
|
100
100
|
oauthProviderName?: string;
|
|
101
101
|
toolCount: number;
|
|
102
102
|
toolNames: string[];
|
|
103
|
+
/** Wall-clock time the server's startup took, whether it connected or failed. */
|
|
104
|
+
startupMs: number;
|
|
103
105
|
error?: string;
|
|
104
106
|
}
|
|
105
107
|
export interface McpManager {
|
|
@@ -65,7 +65,7 @@ Generally speaking, unless it is for very simple requests, you should spawn para
|
|
|
65
65
|
|
|
66
66
|
## Tone and Style
|
|
67
67
|
- Be clear and direct. Give enough context that the user understands what's happening and why
|
|
68
|
-
- Reference cells/ranges frequently using Sheet!Range or Sheet!Cell format (e.g.,
|
|
68
|
+
- Reference cells/ranges frequently using Sheet!Range or Sheet!Cell format as plain text (e.g., Summary!A1:B10, Data!C5). Do not wrap them in markdown links.
|
|
69
69
|
- No emojis unless requested. Avoid filler phrases
|
|
70
70
|
`;
|
|
71
71
|
// ---------------------------------------------------------------------------
|
|
@@ -65,7 +65,7 @@ Launch verification subagent(s) in the background to check results against requi
|
|
|
65
65
|
|
|
66
66
|
- **NEVER** write "based on your findings, implement X" — this pushes synthesis onto the subagent. YOU must synthesize research results and write precise instructions.
|
|
67
67
|
- **NEVER** delegate understanding. Read subagent outputs, extract what matters, then write specific implementation instructions.
|
|
68
|
-
- **NEVER**
|
|
68
|
+
- **NEVER** spawn a replacement until the original subagent has stopped, and never resume the original afterward.
|
|
69
69
|
- **NEVER** give vague instructions like "fix the formatting" — specify which cells, which format, which style.
|
|
70
70
|
|
|
71
71
|
## Delegation Format
|
|
@@ -575,6 +575,7 @@ export class ModelRegistry {
|
|
|
575
575
|
input: modelDef.input,
|
|
576
576
|
cost: modelDef.cost,
|
|
577
577
|
contextWindow: modelDef.contextWindow,
|
|
578
|
+
compactionThresholdTokens: modelDef.compactionThresholdTokens,
|
|
578
579
|
maxTokens: modelDef.maxTokens,
|
|
579
580
|
capabilities: modelDef.capabilities,
|
|
580
581
|
headers,
|