shortcutxl 0.3.90 → 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 +5 -0
- 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/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/providers/model-registry.d.ts +1 -0
- package/dist/app/providers/model-registry.js +1 -0
- package/dist/app/session/session-models.js +1 -0
- package/dist/cli.js +836 -286
- package/dist/contracts/agent-api.d.ts +4 -4
- package/dist/contracts/agent-api.js +4 -4
- package/dist/core/prompts/agent-guidelines.d.ts +1 -1
- package/dist/core/prompts/agent-guidelines.js +1 -1
- 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 +1 -1
- 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/model-registry.d.ts +1 -0
- package/dist/embedded-agent/model-registry.js +1 -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 +29 -0
- package/dist/model-display.js +1 -1
- package/dist/model-ids.d.ts +3 -1
- package/dist/model-ids.js +4 -1
- 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 +37 -4
- 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-Dep1w85F.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 +2 -2
- 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
package/BINARY-INVENTORY.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-10T05:20:31.240Z",
|
|
4
4
|
"package": "shortcutxl",
|
|
5
5
|
"build": {
|
|
6
6
|
"builder": "github-actions",
|
|
7
7
|
"repository": "lyfegame/shortcut",
|
|
8
|
-
"sourceRef": "refs/heads/
|
|
9
|
-
"sourceCommit": "
|
|
10
|
-
"workflowRef": "lyfegame/shortcut/.github/workflows/release-shortcut-cli-npm-package.yml@refs/heads/
|
|
11
|
-
"runId": "
|
|
8
|
+
"sourceRef": "refs/heads/mcp-startup-budget",
|
|
9
|
+
"sourceCommit": "0905764a93d06c695b77bf413eaa2003d6bbd83f",
|
|
10
|
+
"workflowRef": "lyfegame/shortcut/.github/workflows/release-shortcut-cli-npm-package.yml@refs/heads/mcp-startup-budget",
|
|
11
|
+
"runId": "34440542822",
|
|
12
12
|
"runnerOs": "Windows",
|
|
13
13
|
"runnerImage": "win25-vs2026",
|
|
14
|
-
"runnerImageVersion": "
|
|
14
|
+
"runnerImageVersion": "20260907.229.1"
|
|
15
15
|
},
|
|
16
16
|
"binaryExtensions": [
|
|
17
17
|
".dll",
|
|
@@ -550,7 +550,7 @@
|
|
|
550
550
|
},
|
|
551
551
|
{
|
|
552
552
|
"path": "xll/python/Scripts/httpx.exe",
|
|
553
|
-
"sha256": "
|
|
553
|
+
"sha256": "a022dc159b79a3312afeed6f1733da09a8e85523163566f0df9da427319a0040",
|
|
554
554
|
"source": "httpx console launcher installed into embedded Python",
|
|
555
555
|
"version": "see packaged httpx distribution",
|
|
556
556
|
"builtBy": "third-party",
|
|
@@ -558,7 +558,7 @@
|
|
|
558
558
|
},
|
|
559
559
|
{
|
|
560
560
|
"path": "xll/python/Scripts/idna.exe",
|
|
561
|
-
"sha256": "
|
|
561
|
+
"sha256": "8cd6a6078a64ae8e4c826fde0c179ee719401389df7d73832baf0dd3c582ab38",
|
|
562
562
|
"source": "Python package console launcher installed into embedded Python",
|
|
563
563
|
"version": "see owning Python package metadata in site-packages",
|
|
564
564
|
"builtBy": "third-party",
|
|
@@ -566,7 +566,7 @@
|
|
|
566
566
|
},
|
|
567
567
|
{
|
|
568
568
|
"path": "xll/python/Scripts/pip.exe",
|
|
569
|
-
"sha256": "
|
|
569
|
+
"sha256": "4a130513690433dd5e1e502566a7fd2084fc629eb85d08f66b0aad591a012c23",
|
|
570
570
|
"source": "pip console launcher installed into embedded Python",
|
|
571
571
|
"version": "see packaged pip distribution",
|
|
572
572
|
"builtBy": "third-party",
|
|
@@ -574,7 +574,7 @@
|
|
|
574
574
|
},
|
|
575
575
|
{
|
|
576
576
|
"path": "xll/python/Scripts/pip3.13.exe",
|
|
577
|
-
"sha256": "
|
|
577
|
+
"sha256": "4a130513690433dd5e1e502566a7fd2084fc629eb85d08f66b0aad591a012c23",
|
|
578
578
|
"source": "pip console launcher installed into embedded Python",
|
|
579
579
|
"version": "see packaged pip distribution",
|
|
580
580
|
"builtBy": "third-party",
|
|
@@ -582,7 +582,7 @@
|
|
|
582
582
|
},
|
|
583
583
|
{
|
|
584
584
|
"path": "xll/python/Scripts/pip3.exe",
|
|
585
|
-
"sha256": "
|
|
585
|
+
"sha256": "4a130513690433dd5e1e502566a7fd2084fc629eb85d08f66b0aad591a012c23",
|
|
586
586
|
"source": "pip console launcher installed into embedded Python",
|
|
587
587
|
"version": "see packaged pip distribution",
|
|
588
588
|
"builtBy": "third-party",
|
|
@@ -590,7 +590,7 @@
|
|
|
590
590
|
},
|
|
591
591
|
{
|
|
592
592
|
"path": "xll/python/Scripts/pywin32_postinstall.exe",
|
|
593
|
-
"sha256": "
|
|
593
|
+
"sha256": "1e121893a273808b93c23160b171d2055aa4ec72ddb72c92a3d7ebb274115f1d",
|
|
594
594
|
"source": "Python package console launcher installed into embedded Python",
|
|
595
595
|
"version": "see owning Python package metadata in site-packages",
|
|
596
596
|
"builtBy": "third-party",
|
|
@@ -598,7 +598,7 @@
|
|
|
598
598
|
},
|
|
599
599
|
{
|
|
600
600
|
"path": "xll/python/Scripts/pywin32_testall.exe",
|
|
601
|
-
"sha256": "
|
|
601
|
+
"sha256": "d92feb62593764e35c01fa841192e2cbab59bd51b4d33f88f14197527c48ffa2",
|
|
602
602
|
"source": "Python package console launcher installed into embedded Python",
|
|
603
603
|
"version": "see owning Python package metadata in site-packages",
|
|
604
604
|
"builtBy": "third-party",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.91]
|
|
4
|
+
|
|
5
|
+
- **GPT-6 Astra** - GPT-6 Astra is now available in the model selector.
|
|
6
|
+
- **Faster, more reliable MCP startup** - Shortcut now starts MCP integrations faster, retries temporary connection failures, and clearly reports servers that take too long to start.
|
|
7
|
+
|
|
3
8
|
## [0.3.90]
|
|
4
9
|
|
|
5
10
|
- **Skill Forge** - A recursive self-improvement loop for skills. Create or improve personal skills by giving Skill Forge tasks and a grading bar to train, evaluate, and refine against.
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -296,6 +296,8 @@ export interface Model<TApi extends Api> {
|
|
|
296
296
|
cacheWrite: number;
|
|
297
297
|
};
|
|
298
298
|
contextWindow: number;
|
|
299
|
+
/** Optional auto-compaction threshold. Defaults to the runtime-wide threshold policy. */
|
|
300
|
+
compactionThresholdTokens?: number;
|
|
299
301
|
maxTokens: number;
|
|
300
302
|
capabilities?: ModelCapabilities;
|
|
301
303
|
headers?: Record<string, string>;
|
|
@@ -12,12 +12,20 @@
|
|
|
12
12
|
export declare function setSecret(name: string, payload: Record<string, string>): void;
|
|
13
13
|
/** Retrieve a secret payload for a connection. Returns undefined if not found. */
|
|
14
14
|
export declare function getSecret(name: string): Record<string, string> | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Retrieve several connection secrets without serially blocking the Node event loop.
|
|
17
|
+
* Windows uses one PowerShell process for the whole batch so the P/Invoke helper is
|
|
18
|
+
* compiled once. Other platforms perform their native lookups concurrently.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getSecrets(names: readonly string[], timeoutMs?: number): Promise<Map<string, Record<string, string> | undefined>>;
|
|
15
21
|
/** Remove a secret for a connection. Returns true if it existed. */
|
|
16
22
|
export declare function removeSecret(name: string): boolean;
|
|
17
23
|
/** Store a JSON payload in the OS keychain under a custom service namespace. */
|
|
18
24
|
export declare function setNamespacedSecret(service: string, name: string, payload: object): void;
|
|
19
25
|
/** Retrieve a JSON payload from the OS keychain for a custom service namespace. */
|
|
20
26
|
export declare function getNamespacedSecret<T extends object>(service: string, name: string): T | undefined;
|
|
27
|
+
/** Retrieve several secrets through a single asynchronous batch operation where possible. */
|
|
28
|
+
export declare function getNamespacedSecrets<T extends object>(service: string, names: readonly string[], timeoutMs?: number): Promise<Map<string, T | undefined>>;
|
|
21
29
|
/** Remove a JSON payload from the OS keychain for a custom service namespace. */
|
|
22
30
|
export declare function removeNamespacedSecret(service: string, name: string): boolean;
|
|
23
31
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Imports: none from internal layers. Pure shell-out utility.
|
|
10
10
|
*/
|
|
11
|
-
import { execFileSync } from 'child_process';
|
|
11
|
+
import { execFile, execFileSync } from 'child_process';
|
|
12
12
|
const CONNECTION_SERVICE = 'shortcut.connection';
|
|
13
13
|
// =============================================================================
|
|
14
14
|
// Public API
|
|
@@ -21,6 +21,14 @@ export function setSecret(name, payload) {
|
|
|
21
21
|
export function getSecret(name) {
|
|
22
22
|
return getNamespacedSecret(CONNECTION_SERVICE, name);
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Retrieve several connection secrets without serially blocking the Node event loop.
|
|
26
|
+
* Windows uses one PowerShell process for the whole batch so the P/Invoke helper is
|
|
27
|
+
* compiled once. Other platforms perform their native lookups concurrently.
|
|
28
|
+
*/
|
|
29
|
+
export function getSecrets(names, timeoutMs = 10_000) {
|
|
30
|
+
return getNamespacedSecrets(CONNECTION_SERVICE, names, timeoutMs);
|
|
31
|
+
}
|
|
24
32
|
/** Remove a secret for a connection. Returns true if it existed. */
|
|
25
33
|
export function removeSecret(name) {
|
|
26
34
|
return removeNamespacedSecret(CONNECTION_SERVICE, name);
|
|
@@ -61,6 +69,45 @@ export function getNamespacedSecret(service, name) {
|
|
|
61
69
|
}
|
|
62
70
|
return undefined;
|
|
63
71
|
}
|
|
72
|
+
/** Retrieve several secrets through a single asynchronous batch operation where possible. */
|
|
73
|
+
export async function getNamespacedSecrets(service, names, timeoutMs = 10_000) {
|
|
74
|
+
const uniqueNames = [...new Set(names)];
|
|
75
|
+
const result = new Map();
|
|
76
|
+
if (uniqueNames.length === 0)
|
|
77
|
+
return result;
|
|
78
|
+
if (process.platform === 'win32') {
|
|
79
|
+
const targets = uniqueNames.map((name) => `${service}/${name}`);
|
|
80
|
+
const psTargets = targets.map((target) => `'${psEsc(target)}'`).join(', ');
|
|
81
|
+
const ps = `${WIN_CRED_PREAMBLE}
|
|
82
|
+
$result = @{}
|
|
83
|
+
foreach ($target in @(${psTargets})) {
|
|
84
|
+
$value = [CredManager]::Read($target)
|
|
85
|
+
if ($null -ne $value) { $result[$target] = $value }
|
|
86
|
+
}
|
|
87
|
+
$result | ConvertTo-Json -Compress`;
|
|
88
|
+
const raw = await execFileUtf8('powershell', ['-NoProfile', '-EncodedCommand', psEncode(ps)], timeoutMs);
|
|
89
|
+
const values = raw.trim() ? JSON.parse(raw) : {};
|
|
90
|
+
for (const [index, name] of uniqueNames.entries()) {
|
|
91
|
+
result.set(name, parseSecretJson(values[targets[index]]));
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
if (process.platform === 'darwin') {
|
|
96
|
+
await Promise.all(uniqueNames.map(async (name) => {
|
|
97
|
+
try {
|
|
98
|
+
const raw = await execFileUtf8('security', ['find-generic-password', '-a', name, '-s', service, '-w'], timeoutMs);
|
|
99
|
+
result.set(name, parseSecretJson(raw));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
result.set(name, undefined);
|
|
103
|
+
}
|
|
104
|
+
}));
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
for (const name of uniqueNames)
|
|
108
|
+
result.set(name, undefined);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
64
111
|
/** Remove a JSON payload from the OS keychain for a custom service namespace. */
|
|
65
112
|
export function removeNamespacedSecret(service, name) {
|
|
66
113
|
try {
|
|
@@ -115,7 +162,11 @@ export function buildConnectionEnv(connectionNames) {
|
|
|
115
162
|
* PowerShell script that uses P/Invoke to call CredRead/CredWrite/CredDelete
|
|
116
163
|
* from advapi32.dll. Works on every Windows version. No external modules needed.
|
|
117
164
|
*/
|
|
165
|
+
// Secrets are read back as .NET strings but serialized to stdout with the
|
|
166
|
+
// console's OEM codepage, which varies per machine and locale. Force BOM-less
|
|
167
|
+
// UTF-8 so Node decodes non-ASCII values correctly on every host.
|
|
118
168
|
const WIN_CRED_PREAMBLE = `
|
|
169
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
|
119
170
|
Add-Type -TypeDefinition @"
|
|
120
171
|
using System;
|
|
121
172
|
using System.Runtime.InteropServices;
|
|
@@ -161,6 +212,27 @@ public class CredManager {
|
|
|
161
212
|
function psEncode(script) {
|
|
162
213
|
return Buffer.from(script, 'utf16le').toString('base64');
|
|
163
214
|
}
|
|
215
|
+
function execFileUtf8(command, args, timeout) {
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
execFile(command, args, { encoding: 'utf-8', timeout, windowsHide: true }, (error, stdout) => {
|
|
218
|
+
if (error) {
|
|
219
|
+
reject(error);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
resolve(stdout);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function parseSecretJson(raw) {
|
|
227
|
+
if (!raw)
|
|
228
|
+
return undefined;
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(raw.trim());
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
164
236
|
function winCredWrite(target, secret) {
|
|
165
237
|
const ps = `${WIN_CRED_PREAMBLE}\n[CredManager]::Write('${psEsc(target)}', '${psEsc(secret)}')`;
|
|
166
238
|
execFileSync('powershell', ['-NoProfile', '-EncodedCommand', psEncode(ps)], {
|
|
@@ -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.
|