@robota-sdk/agent-command 3.0.0-beta.100
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/CHANGELOG.md +1351 -0
- package/LICENSE +661 -0
- package/README.md +100 -0
- package/dist/node/index.cjs +121 -0
- package/dist/node/index.d.cts +1053 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1053 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +122 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +83 -0
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +597 -0
- package/src/agent/agent-command-module.ts +89 -0
- package/src/agent/agent-command-parser.ts +180 -0
- package/src/agent/agent-command.ts +255 -0
- package/src/agent/index.ts +7 -0
- package/src/background/__tests__/background-command-module.test.ts +261 -0
- package/src/background/background-command-module.ts +52 -0
- package/src/background/background-command.ts +65 -0
- package/src/background/index.ts +6 -0
- package/src/command-module-utils.test.ts +37 -0
- package/src/command-module-utils.ts +35 -0
- package/src/compact/__tests__/compact-command-module.test.ts +181 -0
- package/src/compact/compact-command-module.ts +59 -0
- package/src/compact/compact-command.ts +33 -0
- package/src/compact/index.ts +6 -0
- package/src/context/__tests__/context-command-module.test.ts +502 -0
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +82 -0
- package/src/context/context-command.ts +291 -0
- package/src/context/index.ts +6 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
- package/src/default/__tests__/default-command-modules.test.ts +192 -0
- package/src/default/__tests__/model-exposure.test.ts +269 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
- package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +247 -0
- package/src/default/index.ts +5 -0
- package/src/devices/__tests__/devices-command-module.test.ts +299 -0
- package/src/devices/devices-command-module.ts +308 -0
- package/src/devices/devices-command-port.ts +143 -0
- package/src/devices/index.ts +14 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +146 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +323 -0
- package/src/doctor/doctor-node-deps.ts +88 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +124 -0
- package/src/editor/editor-command-module.ts +53 -0
- package/src/editor/editor-command.ts +58 -0
- package/src/editor/index.ts +7 -0
- package/src/editor/resolve-editor.ts +21 -0
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +203 -0
- package/src/effort/effort-command.ts +136 -0
- package/src/effort/index.ts +6 -0
- package/src/events/__tests__/events-command.test.ts +73 -0
- package/src/events/events-command-module.ts +49 -0
- package/src/events/events-command.ts +69 -0
- package/src/events/index.ts +2 -0
- package/src/exit/__tests__/exit-command-module.test.ts +61 -0
- package/src/exit/exit-command-module.ts +47 -0
- package/src/exit/exit-command.ts +26 -0
- package/src/exit/index.ts +6 -0
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +35 -0
- package/src/git/__tests__/git-command-module.test.ts +111 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/git-command-module.ts +138 -0
- package/src/git/git-commit.ts +254 -0
- package/src/git/index.ts +20 -0
- package/src/goal/__tests__/goal-command.test.ts +80 -0
- package/src/goal/goal-command-module.ts +49 -0
- package/src/goal/goal-command.ts +69 -0
- package/src/goal/index.ts +6 -0
- package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +134 -0
- package/src/help/__tests__/help-command.test.ts +130 -0
- package/src/help/help-command-module.ts +47 -0
- package/src/help/help-command.ts +11 -0
- package/src/help/index.ts +6 -0
- package/src/index.ts +163 -0
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +88 -0
- package/src/language/__tests__/language-command-module.test.ts +126 -0
- package/src/language/index.ts +6 -0
- package/src/language/language-command-module.ts +55 -0
- package/src/language/language-command.ts +52 -0
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
- package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
- package/src/mcp-activation/mcp-activation-command.ts +662 -0
- package/src/mcp-activation/mcp-model-notice.ts +99 -0
- package/src/mcp-activation/mcp-model-status.ts +104 -0
- package/src/memory/__tests__/memory-command-module.test.ts +476 -0
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/index.ts +6 -0
- package/src/memory/memory-command-module.ts +77 -0
- package/src/memory/memory-command.ts +326 -0
- package/src/mode/__tests__/mode-command-module.test.ts +230 -0
- package/src/mode/index.ts +6 -0
- package/src/mode/mode-command-module.ts +55 -0
- package/src/mode/mode-command.ts +67 -0
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +265 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +249 -0
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command-module.test.ts +24 -0
- package/src/peers/__tests__/peers-command.test.ts +399 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +220 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
- package/src/permissions/index.ts +6 -0
- package/src/permissions/permissions-command-module.ts +55 -0
- package/src/permissions/permissions-command.ts +89 -0
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/plugin-command-module.ts +85 -0
- package/src/plugin/plugin-command.ts +271 -0
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +220 -0
- package/src/preset/index.ts +6 -0
- package/src/preset/preset-command-module.ts +80 -0
- package/src/preset/preset-command.ts +148 -0
- package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
- package/src/provider/__tests__/org-policy.test.ts +174 -0
- package/src/provider/__tests__/provider-command-module.test.ts +884 -0
- package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
- package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
- package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +40 -0
- package/src/provider/index.ts +32 -0
- package/src/provider/provider-command-execution.ts +235 -0
- package/src/provider/provider-command-module.ts +82 -0
- package/src/provider/provider-command-profile-lifecycle.ts +239 -0
- package/src/provider/provider-command-profile-operations.ts +218 -0
- package/src/provider/provider-command-profile.ts +116 -0
- package/src/provider/provider-command-setup.ts +79 -0
- package/src/provider/provider-connection-setup.ts +287 -0
- package/src/provider/provider-connection-validation.ts +38 -0
- package/src/provider/provider-onboarding.ts +73 -0
- package/src/provider/provider-prompt-interaction.ts +28 -0
- package/src/provider/provider-setup-ask.ts +67 -0
- package/src/provider/provider-setup-flow.ts +326 -0
- package/src/provider/provider-startup.ts +225 -0
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +63 -0
- package/src/reset/index.ts +2 -0
- package/src/reset/reset-command-module.ts +47 -0
- package/src/reset/reset-command.ts +10 -0
- package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
- package/src/rewind/index.ts +2 -0
- package/src/rewind/rewind-command-module.ts +56 -0
- package/src/rewind/rewind-command.ts +279 -0
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +399 -0
- package/src/schedule/__tests__/loop-command.test.ts +594 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +247 -0
- package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
- package/src/schedule/index.ts +12 -0
- package/src/schedule/loop-command.ts +427 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +184 -0
- package/src/schedule/schedule-command.ts +161 -0
- package/src/schedule/schedule-spec-parser.ts +70 -0
- package/src/session/__tests__/model-pricing.test.ts +66 -0
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +589 -0
- package/src/session/index.ts +17 -0
- package/src/session/model-pricing.ts +26 -0
- package/src/session/session-command-module.ts +197 -0
- package/src/session/session-command.ts +272 -0
- package/src/settings/index.ts +7 -0
- package/src/settings/settings-command-module.ts +47 -0
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
- package/src/shell/index.ts +8 -0
- package/src/shell/resolve-shell.ts +25 -0
- package/src/shell/shell-command-module.ts +53 -0
- package/src/shell/shell-command.ts +48 -0
- package/src/shell/spawn-inherited.ts +32 -0
- package/src/skills/__tests__/skill-read-command.test.ts +64 -0
- package/src/skills/__tests__/skills-command-module.test.ts +200 -0
- package/src/skills/index.ts +7 -0
- package/src/skills/skill-read-command.ts +60 -0
- package/src/skills/skills-command-module.ts +76 -0
- package/src/skills/skills-command.ts +108 -0
- package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
- package/src/statusline/index.ts +6 -0
- package/src/statusline/statusline-command-module.ts +55 -0
- package/src/statusline/statusline-command.ts +80 -0
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
- package/src/terminal-client/index.ts +5 -0
- package/src/terminal-client/terminal-client-commands.ts +109 -0
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +221 -0
- package/src/user-local/__tests__/user-local-command.test.ts +180 -0
- package/src/user-local/index.ts +13 -0
- package/src/user-local/user-local-command-constants.ts +5 -0
- package/src/user-local/user-local-command-module.ts +65 -0
- package/src/user-local/user-local-command.ts +214 -0
- package/src/user-local/user-local-memory-command.ts +160 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { shellArgumentForDisplay } from '@robota-sdk/agent-core';
|
|
2
|
+
|
|
3
|
+
import { mcpModelStatusResult } from './mcp-model-status.js';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ICommandHostAdapterAccess,
|
|
7
|
+
ICommandHostSessionAccess,
|
|
8
|
+
ICommandHostUserInteraction,
|
|
9
|
+
ICommandHostWorkspace,
|
|
10
|
+
ICommandMCPActivationAdapter,
|
|
11
|
+
ICommandMCPActivationSummary,
|
|
12
|
+
ICommandMCPOAuthLoginRequest,
|
|
13
|
+
ICommandMCPOAuthLoginResult,
|
|
14
|
+
ICommandMCPOAuthLogoutResult,
|
|
15
|
+
ICommandMCPOAuthStatus,
|
|
16
|
+
ICommandMCPSourceProblem,
|
|
17
|
+
} from '@robota-sdk/agent-framework';
|
|
18
|
+
import type { ICommandProductVocabulary } from '@robota-sdk/agent-framework';
|
|
19
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
20
|
+
|
|
21
|
+
function adapter(context: ICommandHostAdapterAccess): ICommandMCPActivationAdapter | undefined {
|
|
22
|
+
return context.getCommandHostAdapters?.().mcpActivation;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const OAUTH_STATE_LABEL: Record<ICommandMCPOAuthStatus['state'], string> = {
|
|
26
|
+
'signed-in': 'signed in',
|
|
27
|
+
'expired-refreshable': 'token expired, will refresh',
|
|
28
|
+
'sign-in-required': 'sign-in required',
|
|
29
|
+
'signed-out': 'signed out',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const TOKEN_LABEL = { refresh_token: 'refresh token', access_token: 'access token' } as const;
|
|
33
|
+
|
|
34
|
+
/** What revocation did, by fixed words and reasons only. */
|
|
35
|
+
function revocationText(result: ICommandMCPOAuthLogoutResult): string {
|
|
36
|
+
const failure = result.revocationFailure ?? 'revocation-failed';
|
|
37
|
+
switch (result.revocation) {
|
|
38
|
+
case 'revoked':
|
|
39
|
+
return 'the tokens were revoked';
|
|
40
|
+
case 'not-attempted':
|
|
41
|
+
return 'there were no tokens to revoke';
|
|
42
|
+
case 'unsupported':
|
|
43
|
+
return 'the authorization server offers no token revocation; the tokens stay valid until they expire';
|
|
44
|
+
case 'failed':
|
|
45
|
+
return `token revocation failed (${failure}); the tokens stay valid until they expire`;
|
|
46
|
+
case 'partial':
|
|
47
|
+
return (result.tokens ?? [])
|
|
48
|
+
.map((token) =>
|
|
49
|
+
token.revoked
|
|
50
|
+
? `the ${TOKEN_LABEL[token.token]} was revoked`
|
|
51
|
+
: `the ${TOKEN_LABEL[token.token]} was not (${token.failure ?? 'revocation-failed'}) and stays valid until it expires`,
|
|
52
|
+
)
|
|
53
|
+
.join('; ');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const USAGE =
|
|
58
|
+
'Usage: /mcp [status] | /mcp <approve|reject|revoke|logout> <serverId> | /mcp login <serverId> [--no-browser] | /mcp reload';
|
|
59
|
+
const LOGIN_USAGE = 'Usage: /mcp login <serverId> [--no-browser]';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A server's name as a command argument. It comes from a definition a repository may write, so it
|
|
63
|
+
* is shown only when it is safe to paste into any shell; otherwise `<server>` stands in for it.
|
|
64
|
+
*/
|
|
65
|
+
function serverArgument(serverId: string): string {
|
|
66
|
+
return shellArgumentForDisplay(serverId) ?? '<server>';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** How to sign in to one server: in this session, or from a terminal. */
|
|
70
|
+
function signInHint(serverId: string, cliName?: string): string {
|
|
71
|
+
const argument = serverArgument(serverId);
|
|
72
|
+
const terminal =
|
|
73
|
+
cliName === undefined
|
|
74
|
+
? 'sign in from a terminal'
|
|
75
|
+
: `run ${cliName} mcp login ${argument} in a terminal`;
|
|
76
|
+
const how = `run /mcp login ${argument}, or ${terminal}`;
|
|
77
|
+
return shellArgumentForDisplay(serverId) === undefined
|
|
78
|
+
? ` (${how}; its name cannot be shown safely here)`
|
|
79
|
+
: ` (${how})`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatSummary(
|
|
83
|
+
summary: ICommandMCPActivationSummary,
|
|
84
|
+
oauth: ICommandMCPOAuthStatus['state'] | undefined,
|
|
85
|
+
cliName?: string,
|
|
86
|
+
): string {
|
|
87
|
+
const label = summary.displayName ? ` (${summary.displayName})` : '';
|
|
88
|
+
// A fixed word per state: nothing token-derived ever reaches this line.
|
|
89
|
+
const signIn =
|
|
90
|
+
oauth === undefined
|
|
91
|
+
? ''
|
|
92
|
+
: ` — OAuth: ${OAUTH_STATE_LABEL[oauth]}${oauth === 'sign-in-required' || oauth === 'signed-out' ? signInHint(summary.serverId, cliName) : ''}`;
|
|
93
|
+
return ` ${summary.serverId}${label} — ${summary.status} — ${summary.source} — ${summary.reason}${signIn}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Issue #2794: a source that produced no server names at all — "which file could not be read".
|
|
98
|
+
*
|
|
99
|
+
* `blockedServerNames` (PR #3076 review): a managed-tier problem also blocked every lower-tier
|
|
100
|
+
* server that would otherwise have resolved — those names never appear in `list()` above (an
|
|
101
|
+
* `unresolved` entry is never an activation candidate), so this line is the ONLY place `/mcp status`
|
|
102
|
+
* says they exist at all and why they are not active.
|
|
103
|
+
*/
|
|
104
|
+
function formatSourceProblem(problem: ICommandMCPSourceProblem): string {
|
|
105
|
+
const blocked = problem.blockedServerNames ?? [];
|
|
106
|
+
const blockedNote = blocked.length === 0 ? '' : ` Blocked until fixed: ${blocked.join(', ')}.`;
|
|
107
|
+
return ` ${problem.source} (${problem.origin}) could not be read: ${problem.reason}.${blockedNote}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function oauthStates(
|
|
111
|
+
mcp: ICommandMCPActivationAdapter,
|
|
112
|
+
): Promise<ReadonlyMap<string, ICommandMCPOAuthStatus['state']>> {
|
|
113
|
+
const states = (await mcp.oauthStatus?.()) ?? [];
|
|
114
|
+
return new Map(states.map((status) => [status.serverId, status.state]));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function listResult(
|
|
118
|
+
mcp: ICommandMCPActivationAdapter | undefined,
|
|
119
|
+
cliName?: string,
|
|
120
|
+
): Promise<ICommandResult> {
|
|
121
|
+
if (!mcp) {
|
|
122
|
+
return {
|
|
123
|
+
message: 'MCP activation management is not available in this environment.',
|
|
124
|
+
success: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const entries = mcp.list();
|
|
128
|
+
const oauth = await oauthStates(mcp);
|
|
129
|
+
// A source-scoped problem (issue #2794) names no server, so it never appears in `entries` — it is
|
|
130
|
+
// reported beside them rather than folded into the "no servers" branch, which would otherwise say
|
|
131
|
+
// nothing while a managed policy sits unreadable.
|
|
132
|
+
const sourceProblems = mcp.sourceProblems?.() ?? [];
|
|
133
|
+
const sourceProblemLines =
|
|
134
|
+
sourceProblems.length === 0
|
|
135
|
+
? []
|
|
136
|
+
: [`MCP source problems:\n${sourceProblems.map(formatSourceProblem).join('\n')}`];
|
|
137
|
+
|
|
138
|
+
if (entries.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
message:
|
|
141
|
+
sourceProblemLines.length === 0
|
|
142
|
+
? 'No MCP definitions are registered.'
|
|
143
|
+
: sourceProblemLines[0]!,
|
|
144
|
+
success: true,
|
|
145
|
+
data: { servers: [], sourceProblems },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
message: [
|
|
150
|
+
`MCP activation status:\n${entries
|
|
151
|
+
.map((entry) => formatSummary(entry, oauth.get(entry.serverId), cliName))
|
|
152
|
+
.join('\n')}`,
|
|
153
|
+
...sourceProblemLines,
|
|
154
|
+
].join('\n\n'),
|
|
155
|
+
success: true,
|
|
156
|
+
data: {
|
|
157
|
+
servers: entries.map((entry) => ({
|
|
158
|
+
serverId: entry.serverId,
|
|
159
|
+
...(entry.displayName === undefined ? {} : { displayName: entry.displayName }),
|
|
160
|
+
source: entry.source,
|
|
161
|
+
status: entry.status,
|
|
162
|
+
allowed: entry.allowed,
|
|
163
|
+
provenanceId: entry.provenanceId,
|
|
164
|
+
definitionFingerprint: entry.definitionFingerprint,
|
|
165
|
+
securityIdentity: entry.securityIdentity,
|
|
166
|
+
...(oauth.has(entry.serverId) ? { oauth: oauth.get(entry.serverId) } : {}),
|
|
167
|
+
// #3282 §4 part b-2: the Settings screen's MCP Servers section.
|
|
168
|
+
...(entry.connection === undefined ? {} : { connection: entry.connection }),
|
|
169
|
+
...(entry.connectionFailureReason === undefined
|
|
170
|
+
? {}
|
|
171
|
+
: { connectionFailureReason: entry.connectionFailureReason }),
|
|
172
|
+
...(entry.toolNames === undefined ? {} : { toolNames: entry.toolNames }),
|
|
173
|
+
})),
|
|
174
|
+
sourceProblems,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** What `/mcp` reaches: its port, and — to sign in — the live session and the user. */
|
|
180
|
+
export type TMCPActivationCommandContext = ICommandHostAdapterAccess &
|
|
181
|
+
Pick<ICommandHostSessionAccess, 'getSession'> &
|
|
182
|
+
ICommandHostUserInteraction &
|
|
183
|
+
Partial<Pick<ICommandHostWorkspace, 'getCommandInvocationSource'>> & {
|
|
184
|
+
getCommandSurfaceLocalityEvidence?(): 'local' | 'remote' | undefined;
|
|
185
|
+
getCommandProductVocabulary?(): ICommandProductVocabulary | undefined;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export async function executeMCPActivationCommand(
|
|
189
|
+
context: TMCPActivationCommandContext,
|
|
190
|
+
args: string,
|
|
191
|
+
): Promise<ICommandResult> {
|
|
192
|
+
const trimmed = args.trim();
|
|
193
|
+
const spaceAt = trimmed.indexOf(' ');
|
|
194
|
+
const cliName = context.getCommandProductVocabulary?.()?.cliName;
|
|
195
|
+
const verb = (spaceAt === -1 ? trimmed : trimmed.slice(0, spaceAt)).toLowerCase();
|
|
196
|
+
const serverId = spaceAt === -1 ? '' : trimmed.slice(spaceAt + 1).trim();
|
|
197
|
+
|
|
198
|
+
if (verb.startsWith('skill-')) return skillResult(context, verb, serverId);
|
|
199
|
+
|
|
200
|
+
if (verb === '' || verb === 'status' || verb === 'list') {
|
|
201
|
+
// The full view only for a caller known to be a person; an unknown caller gets the model's view.
|
|
202
|
+
const source = context.getCommandInvocationSource?.();
|
|
203
|
+
return source === 'user' || source === 'remote'
|
|
204
|
+
? listResult(adapter(context), cliName)
|
|
205
|
+
: mcpModelStatusResult(adapter(context), cliName);
|
|
206
|
+
}
|
|
207
|
+
if (verb === 'login') return loginResult(context, serverId);
|
|
208
|
+
if (verb === 'reload') return reloadResult(context);
|
|
209
|
+
|
|
210
|
+
if (verb !== 'approve' && verb !== 'reject' && verb !== 'revoke' && verb !== 'logout') {
|
|
211
|
+
return { message: `Unknown argument. ${USAGE}`, success: false };
|
|
212
|
+
}
|
|
213
|
+
if (!serverId) {
|
|
214
|
+
return {
|
|
215
|
+
message: `Usage: /mcp ${verb} <serverId>`,
|
|
216
|
+
success: false,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const mcp = adapter(context);
|
|
221
|
+
if (!mcp) {
|
|
222
|
+
return {
|
|
223
|
+
message: 'MCP activation management is not available in this environment.',
|
|
224
|
+
success: true,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (verb === 'logout') return logoutResult(mcp, serverId);
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const result = await mcp[verb](serverId);
|
|
231
|
+
const decided = {
|
|
232
|
+
message: `MCP server ${serverId} is now ${result.status}. ${result.reason}`,
|
|
233
|
+
success:
|
|
234
|
+
result.status === 'approved' || result.status === 'rejected' || result.status === 'revoked',
|
|
235
|
+
data: {
|
|
236
|
+
serverId: result.serverId,
|
|
237
|
+
status: result.status,
|
|
238
|
+
allowed: result.allowed,
|
|
239
|
+
source: result.source,
|
|
240
|
+
provenanceId: result.provenanceId,
|
|
241
|
+
definitionFingerprint: result.definitionFingerprint,
|
|
242
|
+
securityIdentity: result.securityIdentity,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
// An approval takes effect now: the session connects the server as `/mcp reload` would.
|
|
246
|
+
if (verb !== 'approve' || result.status !== 'approved' || mcp.reload === undefined) {
|
|
247
|
+
return decided;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const reloaded = await reloadResult(context);
|
|
251
|
+
return {
|
|
252
|
+
...decided,
|
|
253
|
+
message: `${decided.message} ${reloaded.message}`,
|
|
254
|
+
data: { ...decided.data, reload: reloaded.data ?? {} },
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
258
|
+
return {
|
|
259
|
+
...decided,
|
|
260
|
+
message: `${decided.message} Could not connect now: ${reason}. Run /mcp reload to retry.`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return {
|
|
265
|
+
message: error instanceof Error ? error.message : String(error),
|
|
266
|
+
success: false,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** JSON arrays preserve opaque identities; ordinary whitespace-separated arguments remain convenient. */
|
|
272
|
+
function skillArguments(args: string): string[] | undefined {
|
|
273
|
+
if (!args.startsWith('[')) return args.split(/\s+/).filter(Boolean);
|
|
274
|
+
try {
|
|
275
|
+
const parsed: unknown = JSON.parse(args);
|
|
276
|
+
return Array.isArray(parsed) && parsed.every((value) => typeof value === 'string')
|
|
277
|
+
? parsed
|
|
278
|
+
: undefined;
|
|
279
|
+
} catch {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function skillResult(
|
|
285
|
+
context: TMCPActivationCommandContext,
|
|
286
|
+
verb: string,
|
|
287
|
+
rest: string,
|
|
288
|
+
): Promise<ICommandResult> {
|
|
289
|
+
const source = context.getCommandInvocationSource?.();
|
|
290
|
+
const localUser =
|
|
291
|
+
source === 'user' ||
|
|
292
|
+
(source === 'remote' && context.getCommandSurfaceLocalityEvidence?.() === 'local');
|
|
293
|
+
if (verb !== 'skill-list' && !localUser)
|
|
294
|
+
return {
|
|
295
|
+
success: false,
|
|
296
|
+
message: 'Only a known local user can inspect, approve or withdraw MCP skill instructions.',
|
|
297
|
+
};
|
|
298
|
+
const words = skillArguments(rest);
|
|
299
|
+
const count = verb === 'skill-list' ? 1 : verb === 'skill-approve' ? 3 : 2;
|
|
300
|
+
if (
|
|
301
|
+
!['skill-list', 'skill-inspect', 'skill-approve', 'skill-withdraw'].includes(verb) ||
|
|
302
|
+
!words ||
|
|
303
|
+
words.length !== count ||
|
|
304
|
+
words.some((word) => word.length === 0)
|
|
305
|
+
)
|
|
306
|
+
return {
|
|
307
|
+
success: false,
|
|
308
|
+
message:
|
|
309
|
+
'Usage: /mcp skill-list <server> | skill-inspect <server> <uri> | skill-approve <server> <uri> <fingerprint> | skill-withdraw <server> <uri>. JSON string arrays also accept opaque arguments.',
|
|
310
|
+
};
|
|
311
|
+
const skills = adapter(context)?.skills;
|
|
312
|
+
if (!skills)
|
|
313
|
+
return { success: false, message: 'MCP Skills are not available in this environment.' };
|
|
314
|
+
const serverId = words[0]!;
|
|
315
|
+
const uri = words[1]!;
|
|
316
|
+
try {
|
|
317
|
+
if (verb === 'skill-list') {
|
|
318
|
+
const entries = await skills.list(serverId);
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
message:
|
|
322
|
+
entries.length === 0
|
|
323
|
+
? 'No MCP skills are advertised.'
|
|
324
|
+
: entries
|
|
325
|
+
.map(
|
|
326
|
+
(entry) =>
|
|
327
|
+
`${entry.name} — ${entry.uri}\n${entry.description}` +
|
|
328
|
+
(entry.invocationName ? `\nInvoke skill: ${entry.invocationName}` : '') +
|
|
329
|
+
(entry.unavailableReason ? `\nUnavailable: ${entry.unavailableReason}` : ''),
|
|
330
|
+
)
|
|
331
|
+
.join('\n\n'),
|
|
332
|
+
data: {
|
|
333
|
+
skills: entries.map((entry) => ({
|
|
334
|
+
serverId: entry.serverId,
|
|
335
|
+
uri: entry.uri,
|
|
336
|
+
name: entry.name,
|
|
337
|
+
description: entry.description,
|
|
338
|
+
...(entry.invocationName ? { invocationName: entry.invocationName } : {}),
|
|
339
|
+
...(entry.unavailableReason ? { unavailableReason: entry.unavailableReason } : {}),
|
|
340
|
+
})),
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (verb === 'skill-withdraw') {
|
|
345
|
+
skills.withdraw(serverId, uri, 'user');
|
|
346
|
+
return { success: true, message: 'MCP skill content consent withdrawn.' };
|
|
347
|
+
}
|
|
348
|
+
const preview =
|
|
349
|
+
verb === 'skill-approve'
|
|
350
|
+
? await skills.approve(serverId, uri, words[2]!, 'user')
|
|
351
|
+
: await skills.inspect(serverId, uri);
|
|
352
|
+
const args = [serverId, uri, preview.fingerprint];
|
|
353
|
+
const approveArgs = args.every((arg) => /^[\w:./%-]+$/.test(arg))
|
|
354
|
+
? args.join(' ')
|
|
355
|
+
: JSON.stringify(args);
|
|
356
|
+
return {
|
|
357
|
+
success: true,
|
|
358
|
+
message:
|
|
359
|
+
verb === 'skill-approve'
|
|
360
|
+
? 'MCP skill content consent saved; activation remains a separate action.'
|
|
361
|
+
: `${preview.content}\n\nFrontmatter: ${JSON.stringify(preview.frontmatter)}\n\nApprove exactly this content: /mcp skill-approve ${approveArgs}`,
|
|
362
|
+
data: {
|
|
363
|
+
serverId: preview.serverId,
|
|
364
|
+
uri: preview.uri,
|
|
365
|
+
namespace: preview.namespace,
|
|
366
|
+
fingerprint: preview.fingerprint,
|
|
367
|
+
frontmatter: preview.frontmatter,
|
|
368
|
+
...(verb === 'skill-inspect' ? { content: preview.content } : {}),
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
} catch {
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
message:
|
|
375
|
+
'MCP skill request refused. Check /mcp status; run /mcp skill-inspect <server> <uri> before granting content consent.',
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function logoutResult(
|
|
381
|
+
mcp: ICommandMCPActivationAdapter,
|
|
382
|
+
serverId: string,
|
|
383
|
+
): Promise<ICommandResult> {
|
|
384
|
+
if (mcp.oauthLogout === undefined || (await oauthStates(mcp)).get(serverId) === undefined) {
|
|
385
|
+
return { message: `MCP server ${serverId} does not declare OAuth.`, success: false };
|
|
386
|
+
}
|
|
387
|
+
let result: ICommandMCPOAuthLogoutResult;
|
|
388
|
+
try {
|
|
389
|
+
result = await mcp.oauthLogout(serverId);
|
|
390
|
+
} catch {
|
|
391
|
+
return { message: `Signing out of MCP server ${serverId} failed.`, success: false };
|
|
392
|
+
}
|
|
393
|
+
const signedOut = result.removed
|
|
394
|
+
? `Signed out of MCP server ${serverId}`
|
|
395
|
+
: `MCP server ${serverId} was not signed in`;
|
|
396
|
+
return {
|
|
397
|
+
message: `${signedOut}; ${revocationText(result)}.`,
|
|
398
|
+
success: true,
|
|
399
|
+
data: { ...result },
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* #3282 §4 part b-2: `/mcp reload` and the Settings screen's "Reload servers" button — retries
|
|
405
|
+
* every server not currently connected and adds whatever tools it now offers to the live session,
|
|
406
|
+
* the same way a completed sign-in does.
|
|
407
|
+
*/
|
|
408
|
+
async function reloadResult(context: TMCPActivationCommandContext): Promise<ICommandResult> {
|
|
409
|
+
const mcp = adapter(context);
|
|
410
|
+
if (!mcp) {
|
|
411
|
+
return {
|
|
412
|
+
message: 'MCP activation management is not available in this environment.',
|
|
413
|
+
success: true,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (mcp.reload === undefined) {
|
|
417
|
+
return {
|
|
418
|
+
message: 'Reloading MCP servers is not available in this environment.',
|
|
419
|
+
success: false,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const result = await mcp.reload();
|
|
423
|
+
let added: readonly string[] = [];
|
|
424
|
+
try {
|
|
425
|
+
added = result.tools.length === 0 ? [] : await context.getSession().addTools(result.tools);
|
|
426
|
+
} finally {
|
|
427
|
+
if (result.reloadToken !== undefined) mcp.reloadToolsAdded?.(result.reloadToken, added);
|
|
428
|
+
}
|
|
429
|
+
const dropped = result.tools.length - added.length;
|
|
430
|
+
const parts = [
|
|
431
|
+
result.connectedServerIds.length === 0
|
|
432
|
+
? 'No server newly connected.'
|
|
433
|
+
: `Connected: ${result.connectedServerIds.join(', ')}.`,
|
|
434
|
+
result.failedServerIds.length === 0
|
|
435
|
+
? undefined
|
|
436
|
+
: `Still not connected: ${result.failedServerIds.join(', ')} (see /mcp status).`,
|
|
437
|
+
dropped === 0
|
|
438
|
+
? undefined
|
|
439
|
+
: `${dropped} of the new tools ${dropped === 1 ? 'was' : 'were'} left out: the session already has a tool by that name.`,
|
|
440
|
+
].filter((part): part is string => part !== undefined);
|
|
441
|
+
return {
|
|
442
|
+
message: `Reloaded MCP servers. ${parts.join(' ')}`.trim(),
|
|
443
|
+
success: true,
|
|
444
|
+
data: {
|
|
445
|
+
connectedServerIds: result.connectedServerIds,
|
|
446
|
+
failedServerIds: result.failedServerIds,
|
|
447
|
+
tools: [...added],
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let loginPrompts = 0;
|
|
453
|
+
|
|
454
|
+
/** Before the browser opens: the URL it will show, and the choice to paste or cancel instead. */
|
|
455
|
+
function browserConfirmer(
|
|
456
|
+
context: ICommandHostUserInteraction,
|
|
457
|
+
serverId: string,
|
|
458
|
+
): ICommandMCPOAuthLoginRequest['confirmBrowser'] {
|
|
459
|
+
const ui = context.getUserInteraction();
|
|
460
|
+
if (ui === undefined) return undefined;
|
|
461
|
+
return async (prompt, signal) => {
|
|
462
|
+
// The ask port cannot withdraw a question, so a sign-in that has already ended is not asked
|
|
463
|
+
// about, and an answer that arrives after it ended never opens a browser for it.
|
|
464
|
+
if (signal.aborted) return 'cancel';
|
|
465
|
+
loginPrompts += 1;
|
|
466
|
+
const answer = await ui.ask({
|
|
467
|
+
id: `mcp-login-browser-${loginPrompts}`,
|
|
468
|
+
title: `Sign in to MCP server ${serverArgument(serverId)}`,
|
|
469
|
+
description:
|
|
470
|
+
`Your browser will open this URL; if it does not, open it yourself:\n${prompt.authorizationUrl}\n\n` +
|
|
471
|
+
'The sign-in completes on its own once you approve in the browser.',
|
|
472
|
+
options: [
|
|
473
|
+
{ value: 'open', label: 'Open my browser' },
|
|
474
|
+
{ value: 'paste', label: 'Paste the redirect instead (browser on another machine)' },
|
|
475
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
476
|
+
],
|
|
477
|
+
maxSelect: 1,
|
|
478
|
+
default: { values: ['open'] },
|
|
479
|
+
});
|
|
480
|
+
if (answer.type !== 'answer' || signal.aborted) return 'cancel';
|
|
481
|
+
const choice = answer.values[0];
|
|
482
|
+
return choice === 'open' || choice === 'paste' ? choice : 'cancel';
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Asks, through the session's own prompt, for the redirect URL the user's browser was sent to. */
|
|
487
|
+
function redirectReader(
|
|
488
|
+
context: ICommandHostUserInteraction,
|
|
489
|
+
serverId: string,
|
|
490
|
+
): ICommandMCPOAuthLoginRequest['readRedirect'] {
|
|
491
|
+
const ui = context.getUserInteraction();
|
|
492
|
+
if (ui === undefined) return undefined;
|
|
493
|
+
return async (prompt, signal) => {
|
|
494
|
+
// A sign-in that has already ended (cancelled or timed out) does not ask for a paste.
|
|
495
|
+
signal.throwIfAborted();
|
|
496
|
+
loginPrompts += 1;
|
|
497
|
+
const answer = await ui.ask({
|
|
498
|
+
id: `mcp-login-redirect-${loginPrompts}`,
|
|
499
|
+
title: `Sign in to MCP server ${serverArgument(serverId)}`,
|
|
500
|
+
description:
|
|
501
|
+
`Open this URL in a browser:\n${prompt.authorizationUrl}\n\n` +
|
|
502
|
+
`After you approve, the browser is sent to ${prompt.redirectUri}, which may not load. ` +
|
|
503
|
+
'Copy the full address from its address bar and paste it here.',
|
|
504
|
+
allowFreeText: true,
|
|
505
|
+
masked: true,
|
|
506
|
+
placeholder: 'Redirect URL (not shown)',
|
|
507
|
+
});
|
|
508
|
+
if (answer.type !== 'answer' || answer.text === undefined || answer.text.trim() === '') {
|
|
509
|
+
throw new Error('No redirect URL was pasted.');
|
|
510
|
+
}
|
|
511
|
+
return answer.text;
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Why a sign-in did not complete, and what to run instead, by fixed words only. */
|
|
516
|
+
function loginFailureText(result: ICommandMCPOAuthLoginResult, cliName?: string): string {
|
|
517
|
+
const { serverId, failure } = result;
|
|
518
|
+
const argument = serverArgument(serverId);
|
|
519
|
+
switch (failure) {
|
|
520
|
+
case 'not-oauth':
|
|
521
|
+
return `MCP server ${serverId} does not declare OAuth; there is nothing to sign in to.`;
|
|
522
|
+
case 'url-unset':
|
|
523
|
+
return `MCP server ${serverId} has an unset variable in its url.`;
|
|
524
|
+
case 'sign-in-in-progress':
|
|
525
|
+
return `A sign-in to MCP server ${serverId} is already in progress.`;
|
|
526
|
+
case 'prompt-unavailable':
|
|
527
|
+
if (cliName === undefined) {
|
|
528
|
+
return 'Signing in without a browser needs a redirect URL pasted in a terminal, but no product command name is available in this session.';
|
|
529
|
+
}
|
|
530
|
+
return (
|
|
531
|
+
'Signing in without a browser needs the redirect URL pasted here, and nothing here can ask ' +
|
|
532
|
+
`for it; run ${cliName} mcp login ${argument} --no-browser in a terminal.`
|
|
533
|
+
);
|
|
534
|
+
case 'browser-failed':
|
|
535
|
+
return (
|
|
536
|
+
`Sign-in to MCP server ${serverId} failed (browser-failed): no browser could be opened. ` +
|
|
537
|
+
`Run /mcp login ${argument} --no-browser to paste the redirect instead.`
|
|
538
|
+
);
|
|
539
|
+
default: {
|
|
540
|
+
const secret =
|
|
541
|
+
failure === 'token-exchange-failed' && result.preRegisteredClient
|
|
542
|
+
? cliName === undefined
|
|
543
|
+
? ' If its pre-registered client needs a secret, sign in from a terminal configured for this product.'
|
|
544
|
+
: ` If its pre-registered client needs a secret, run ${cliName} mcp login ${argument} --client-secret in a terminal.`
|
|
545
|
+
: '';
|
|
546
|
+
return `Sign-in to MCP server ${serverId} failed (${failure ?? 'unexpected-error'}); nothing was changed.${secret}`;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** What a completed sign-in did to this session. */
|
|
552
|
+
function loginSuccessText(
|
|
553
|
+
serverId: string,
|
|
554
|
+
connection: ICommandMCPOAuthLoginResult['connection'],
|
|
555
|
+
added: number,
|
|
556
|
+
): string {
|
|
557
|
+
switch (connection) {
|
|
558
|
+
case 'connected':
|
|
559
|
+
return added === 0
|
|
560
|
+
? `Signed in to MCP server ${serverId}; it is connected and offers no new tools.`
|
|
561
|
+
: `Signed in to MCP server ${serverId}; ${added} of its tools ${added === 1 ? 'is' : 'are'} available from your next message.`;
|
|
562
|
+
case 'recovered':
|
|
563
|
+
return `Signed in to MCP server ${serverId}; its tools work again.`;
|
|
564
|
+
case 'not-admitted':
|
|
565
|
+
return `Signed in to MCP server ${serverId}, but it is not approved for this session (see /mcp status).`;
|
|
566
|
+
default:
|
|
567
|
+
return `Signed in to MCP server ${serverId}, but it could not connect in this session; the next session connects it.`;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function loginResult(
|
|
572
|
+
context: TMCPActivationCommandContext,
|
|
573
|
+
rest: string,
|
|
574
|
+
): Promise<ICommandResult> {
|
|
575
|
+
const cliName = context.getCommandProductVocabulary?.()?.cliName;
|
|
576
|
+
const words = rest.split(/\s+/).filter((word) => word !== '');
|
|
577
|
+
const noBrowser = words.includes('--no-browser');
|
|
578
|
+
const withSecret = words.includes('--client-secret');
|
|
579
|
+
const positional = words.filter((word) => word !== '--no-browser' && word !== '--client-secret');
|
|
580
|
+
const serverId = positional[0];
|
|
581
|
+
if (positional.length !== 1 || serverId === undefined || serverId.startsWith('-')) {
|
|
582
|
+
return { message: LOGIN_USAGE, success: false };
|
|
583
|
+
}
|
|
584
|
+
const argument = serverArgument(serverId);
|
|
585
|
+
if (withSecret) {
|
|
586
|
+
// A secret typed here would become part of the conversation; it is asked for only in a terminal.
|
|
587
|
+
return {
|
|
588
|
+
message:
|
|
589
|
+
cliName === undefined
|
|
590
|
+
? 'A client secret is never typed into a session. Sign in from a terminal configured for this product.'
|
|
591
|
+
: `A client secret is never typed into a session. Run ${cliName} mcp login ${argument} --client-secret in a terminal.`,
|
|
592
|
+
success: false,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
const mcp = adapter(context);
|
|
596
|
+
if (!mcp) {
|
|
597
|
+
return {
|
|
598
|
+
message: 'MCP activation management is not available in this environment.',
|
|
599
|
+
success: true,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
if (mcp.oauthLogin === undefined) {
|
|
603
|
+
return {
|
|
604
|
+
message:
|
|
605
|
+
cliName === undefined
|
|
606
|
+
? 'Signing in is not available in this session; sign in from a terminal configured for this product.'
|
|
607
|
+
: `Signing in is not available in this session; run ${cliName} mcp login ${argument} in a terminal.`,
|
|
608
|
+
success: false,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
// A dismissed prompt cancels the sign-in itself, not only the read it was for.
|
|
612
|
+
const cancel = new AbortController();
|
|
613
|
+
const readRedirect = redirectReader(context, serverId);
|
|
614
|
+
const confirmBrowser = noBrowser ? undefined : browserConfirmer(context, serverId);
|
|
615
|
+
const result = await mcp.oauthLogin({
|
|
616
|
+
serverId,
|
|
617
|
+
noBrowser,
|
|
618
|
+
signal: cancel.signal,
|
|
619
|
+
...(readRedirect === undefined
|
|
620
|
+
? {}
|
|
621
|
+
: {
|
|
622
|
+
readRedirect: (prompt, signal) =>
|
|
623
|
+
readRedirect(prompt, signal).catch((error: unknown) => {
|
|
624
|
+
cancel.abort();
|
|
625
|
+
throw error;
|
|
626
|
+
}),
|
|
627
|
+
}),
|
|
628
|
+
...(confirmBrowser === undefined
|
|
629
|
+
? {}
|
|
630
|
+
: {
|
|
631
|
+
confirmBrowser: async (prompt, signal) => {
|
|
632
|
+
const choice = await confirmBrowser(prompt, signal);
|
|
633
|
+
if (choice === 'cancel') cancel.abort();
|
|
634
|
+
return choice;
|
|
635
|
+
},
|
|
636
|
+
}),
|
|
637
|
+
});
|
|
638
|
+
if (result.failure !== undefined) {
|
|
639
|
+
return {
|
|
640
|
+
message: loginFailureText(result, cliName),
|
|
641
|
+
success: false,
|
|
642
|
+
data: { serverId, failure: result.failure },
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
const added = result.tools.length === 0 ? [] : await context.getSession().addTools(result.tools);
|
|
646
|
+
if (result.tools.length > 0) mcp.oauthToolsAdded?.(serverId, added);
|
|
647
|
+
const dropped = result.tools.length - added.length;
|
|
648
|
+
const droppedText =
|
|
649
|
+
dropped === 0
|
|
650
|
+
? ''
|
|
651
|
+
: ` ${dropped} of its tools ${dropped === 1 ? 'was' : 'were'} left out: the session already has a tool by that name.`;
|
|
652
|
+
return {
|
|
653
|
+
message: `${loginSuccessText(serverId, result.connection, added.length)}${droppedText}`,
|
|
654
|
+
success: true,
|
|
655
|
+
data: {
|
|
656
|
+
serverId,
|
|
657
|
+
connection: result.connection ?? 'not-connected',
|
|
658
|
+
tools: [...added],
|
|
659
|
+
...(dropped === 0 ? {} : { dropped }),
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
}
|