@zgeoff/atc 2.37.0 → 2.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents/build-claude-config-seed.ts +7 -4
- package/src/agents/build-claude-mcp-config.ts +27 -0
- package/src/agents/claude-adapter.ts +15 -4
- package/src/build-migrated-config.ts +16 -4
- package/src/shared/collect-agents.ts +31 -8
- package/src/shared/collect-claude-auth.ts +117 -4
- package/src/shared/collect-legacy-agents.ts +3 -0
package/package.json
CHANGED
|
@@ -4,10 +4,12 @@ import { CLAUDE_CONFIG_SEED_FILE } from './claude-config-seed-file';
|
|
|
4
4
|
* The guest file that seeds a session's own Claude config folder with the
|
|
5
5
|
* state the CLI reads from `.claude.json` to start without its first-run
|
|
6
6
|
* onboarding, and with folder trust for `trustedRoot`, the exact root of a
|
|
7
|
-
* verified clone, when one is given.
|
|
7
|
+
* verified clone, when one is given. Trust also approves every MCP server
|
|
8
|
+
* the clone's own `.mcp.json` declares, so an unattended session never
|
|
9
|
+
* waits on the CLI's approval prompt. It holds no account: the placeholder
|
|
8
10
|
* credential is what keeps the CLI from asking for a login. Without a
|
|
9
|
-
* trusted root the CLI asks a person to trust the workspace
|
|
10
|
-
* start.
|
|
11
|
+
* trusted root the CLI asks a person to trust the workspace, and to approve
|
|
12
|
+
* the clone's MCP servers, on its first start.
|
|
11
13
|
*/
|
|
12
14
|
export function buildClaudeConfigSeed(
|
|
13
15
|
trustedRoot: string | null,
|
|
@@ -15,9 +17,10 @@ export function buildClaudeConfigSeed(
|
|
|
15
17
|
const seed =
|
|
16
18
|
trustedRoot === null
|
|
17
19
|
? ONBOARDED_CONFIG
|
|
18
|
-
: { ...ONBOARDED_CONFIG, projects: { [trustedRoot]:
|
|
20
|
+
: { ...ONBOARDED_CONFIG, projects: { [trustedRoot]: TRUSTED_PROJECT } };
|
|
19
21
|
|
|
20
22
|
return { [CLAUDE_CONFIG_SEED_FILE]: JSON.stringify(seed, null, 2) };
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const ONBOARDED_CONFIG = { hasCompletedOnboarding: true };
|
|
26
|
+
const TRUSTED_PROJECT = { hasTrustDialogAccepted: true, enableAllProjectMcpServers: true };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ClaudeMCPServer } from '../shared/collect-claude-auth';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The MCP config file that gives a session each server over HTTP with the
|
|
5
|
+
* broker's placeholder as a bearer credential in the server's header, so
|
|
6
|
+
* impd's broker swaps in the credential it holds for the server's host and
|
|
7
|
+
* the session never holds it.
|
|
8
|
+
*/
|
|
9
|
+
export function buildClaudeMCPConfig(servers: readonly ClaudeMCPServer[]): {
|
|
10
|
+
readonly mcpServers: Readonly<Record<string, unknown>>;
|
|
11
|
+
} {
|
|
12
|
+
return {
|
|
13
|
+
mcpServers: Object.fromEntries(
|
|
14
|
+
servers.map((server) => [
|
|
15
|
+
server.name,
|
|
16
|
+
{
|
|
17
|
+
type: 'http',
|
|
18
|
+
url: server.url,
|
|
19
|
+
headers: { [server.header]: `Bearer ${BROKER_PLACEHOLDER}` },
|
|
20
|
+
},
|
|
21
|
+
]),
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The value impd's broker replaces with the credential on the host's side.
|
|
27
|
+
const BROKER_PLACEHOLDER = 'imp-broker-placeholder';
|
|
@@ -32,6 +32,7 @@ import { buildArgsWithoutFlags } from './build-args-without-flags';
|
|
|
32
32
|
import { buildATCBridgeFiles } from './build-atc-bridge-files';
|
|
33
33
|
import { buildClaudeConfigSeed } from './build-claude-config-seed';
|
|
34
34
|
import { buildClaudeGuestLaunch } from './build-claude-guest-launch';
|
|
35
|
+
import { buildClaudeMCPConfig } from './build-claude-mcp-config';
|
|
35
36
|
import { buildClaudeOverrideArgs } from './build-claude-override-args';
|
|
36
37
|
import { buildHookSettings } from './build-hook-settings';
|
|
37
38
|
import type { HookSettingsProfile } from './build-hook-settings';
|
|
@@ -232,7 +233,11 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
232
233
|
// config bundle, come from the host's own Claude config folder as it is
|
|
233
234
|
// at this launch, staged under a key of this launch's own. A credential, endpoint, or provider variable in the
|
|
234
235
|
// configured settings, or in the host's environment, would keep the CLI
|
|
235
|
-
// from sending the placeholder, so either refuses the start.
|
|
236
|
+
// from sending the placeholder, so either refuses the start. The entry's
|
|
237
|
+
// MCP servers reach the CLI through an MCP config file of the binding
|
|
238
|
+
// revision, each with the placeholder in its header. Their flag sits
|
|
239
|
+
// ahead of the settings flag, so the variadic flag never takes the
|
|
240
|
+
// prompt as one of its values.
|
|
236
241
|
private planSubscriptionGuestSpawn(
|
|
237
242
|
opts: SpawnOptions,
|
|
238
243
|
dir: string,
|
|
@@ -247,6 +252,9 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
const settingsPath = `auth-r${auth.revision}/settings.json`;
|
|
255
|
+
const mcpServers = this.entry.mcpServers ?? [];
|
|
256
|
+
const mcpPath = `auth-r${auth.revision}/mcp.json`;
|
|
257
|
+
const mcpArgs = mcpServers.length === 0 ? [] : ['--mcp-config', `${dir}/${mcpPath}`];
|
|
250
258
|
const bundleKey = randomUUID();
|
|
251
259
|
|
|
252
260
|
const launch = buildClaudeGuestLaunch(
|
|
@@ -256,7 +264,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
256
264
|
...this.buildArgs(
|
|
257
265
|
buildArgsWithoutFlags(this.entry.args, ['--permission-mode']),
|
|
258
266
|
opts,
|
|
259
|
-
|
|
267
|
+
mcpArgs,
|
|
260
268
|
`${dir}/${settingsPath}`,
|
|
261
269
|
`${dir}/atc-bridge`,
|
|
262
270
|
),
|
|
@@ -284,6 +292,9 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
284
292
|
args: launch.args,
|
|
285
293
|
files: {
|
|
286
294
|
[settingsPath]: JSON.stringify(settings, null, 2),
|
|
295
|
+
...(mcpServers.length === 0
|
|
296
|
+
? {}
|
|
297
|
+
: { [mcpPath]: JSON.stringify(buildClaudeMCPConfig(mcpServers), null, 2) }),
|
|
287
298
|
...buildClaudeConfigSeed(null),
|
|
288
299
|
...Object.fromEntries(
|
|
289
300
|
Object.entries(bundle).map(([path, content]) => [
|
|
@@ -365,13 +376,13 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
365
376
|
private buildArgs(
|
|
366
377
|
configured: readonly string[],
|
|
367
378
|
opts: SpawnOptions,
|
|
368
|
-
|
|
379
|
+
leadArgs: readonly string[],
|
|
369
380
|
settings: string,
|
|
370
381
|
pluginDir: string,
|
|
371
382
|
): string[] {
|
|
372
383
|
return [
|
|
373
384
|
...buildClaudeOverrideArgs(configured, opts),
|
|
374
|
-
...
|
|
385
|
+
...leadArgs,
|
|
375
386
|
'--settings',
|
|
376
387
|
settings,
|
|
377
388
|
'--plugin-dir',
|
|
@@ -113,10 +113,22 @@ function renderAgentEntry(entry: AgentEntry): Record<string, unknown> {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
if (entry.auth !== undefined) {
|
|
116
|
-
rendered['auth'] =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
rendered['auth'] = {
|
|
117
|
+
profiles: entry.auth.profiles,
|
|
118
|
+
...(Object.keys(entry.auth.placeholderEnv).length > 0
|
|
119
|
+
? { placeholderEnv: entry.auth.placeholderEnv }
|
|
120
|
+
: {}),
|
|
121
|
+
...(entry.mcpServers === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: {
|
|
124
|
+
mcpServers: Object.fromEntries(
|
|
125
|
+
entry.mcpServers.map((server) => [
|
|
126
|
+
server.name,
|
|
127
|
+
{ url: server.url, profile: server.profile },
|
|
128
|
+
]),
|
|
129
|
+
),
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
return rendered;
|
|
@@ -3,6 +3,7 @@ import { checkGatewayAuth } from './check-gateway-auth';
|
|
|
3
3
|
import type { GatewayAuth } from './check-gateway-auth';
|
|
4
4
|
import type { AuthProfile } from './collect-auth-profiles';
|
|
5
5
|
import { collectClaudeAuth } from './collect-claude-auth';
|
|
6
|
+
import type { ClaudeMCPServer } from './collect-claude-auth';
|
|
6
7
|
import { isSubscriptionOverrideVariable } from './is-subscription-override-variable';
|
|
7
8
|
import { isRecord } from './report';
|
|
8
9
|
|
|
@@ -16,7 +17,8 @@ type AgentKind = 'claude' | 'codex' | 'grok';
|
|
|
16
17
|
* and arguments it starts with, and, for a Claude entry, the settings, the
|
|
17
18
|
* environment, and the backend and credential it runs against. A Claude entry
|
|
18
19
|
* with a `baseURL` is a gateway; without one it is stock Claude, whose
|
|
19
|
-
* `auth` holds profiles alone, for the subscription sign-in
|
|
20
|
+
* `auth` holds profiles alone, for the subscription sign-in, and whose
|
|
21
|
+
* `mcpServers` reach their hosts through those profiles.
|
|
20
22
|
*/
|
|
21
23
|
export interface AgentEntry {
|
|
22
24
|
readonly id: AgentID;
|
|
@@ -30,6 +32,7 @@ export interface AgentEntry {
|
|
|
30
32
|
readonly baseURL?: string;
|
|
31
33
|
readonly apiKeyHelper?: string;
|
|
32
34
|
readonly auth?: GatewayAuth;
|
|
35
|
+
readonly mcpServers?: readonly ClaudeMCPServer[];
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
interface CollectedAgents {
|
|
@@ -43,7 +46,8 @@ interface CollectedAgents {
|
|
|
43
46
|
* Reads the `agents` map into registry order. An entry that is not an object,
|
|
44
47
|
* sets an unknown field or a field outside its kind, holds a wrong-typed
|
|
45
48
|
* value, or fails an auth check is left out with every problem that refused
|
|
46
|
-
* it, and the other entries load.
|
|
49
|
+
* it, and the other entries load. An MCP server that breaks a rule is left
|
|
50
|
+
* out of its entry with an error, and the entry loads. A value that is not an object leaves the
|
|
47
51
|
* registry empty.
|
|
48
52
|
*/
|
|
49
53
|
export function collectAgents(
|
|
@@ -64,6 +68,7 @@ export function collectAgents(
|
|
|
64
68
|
errors.push(...parsed.problems.map((problem) => `agents.${id}: ${problem}`));
|
|
65
69
|
} else {
|
|
66
70
|
agents.push(parsed.entry);
|
|
71
|
+
errors.push(...(parsed.warnings ?? []).map((warning) => `agents.${id}: ${warning}`));
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -86,7 +91,9 @@ const DEFAULT_LABELS: Readonly<Record<AgentKind, string>> = {
|
|
|
86
91
|
grok: 'Grok',
|
|
87
92
|
};
|
|
88
93
|
|
|
89
|
-
type ParsedEntry =
|
|
94
|
+
type ParsedEntry =
|
|
95
|
+
| { readonly entry: AgentEntry; readonly warnings?: readonly string[] }
|
|
96
|
+
| { readonly problems: string[] };
|
|
90
97
|
|
|
91
98
|
function parseAgentEntry(
|
|
92
99
|
id: string,
|
|
@@ -125,7 +132,9 @@ function parseAgentEntry(
|
|
|
125
132
|
return claude;
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
|
|
135
|
+
const { warnings, ...read } = claude;
|
|
136
|
+
|
|
137
|
+
return { entry: { ...entry, ...read }, ...(warnings === undefined ? {} : { warnings }) };
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
// The entry's kind, or the problem that refuses it. An absent kind is the id
|
|
@@ -251,6 +260,12 @@ function buildDefaultLabel(id: string, kind: AgentKind): string {
|
|
|
251
260
|
return id === kind ? DEFAULT_LABELS[kind] : id;
|
|
252
261
|
}
|
|
253
262
|
|
|
263
|
+
interface ReadClaudeAuth {
|
|
264
|
+
readonly auth?: GatewayAuth;
|
|
265
|
+
readonly mcpServers?: readonly ClaudeMCPServer[];
|
|
266
|
+
readonly warnings?: readonly string[];
|
|
267
|
+
}
|
|
268
|
+
|
|
254
269
|
// The `auth` a Claude entry holds, or every problem with it. With a base URL
|
|
255
270
|
// the entry is a gateway and its auth is the gateway's. Without one it is
|
|
256
271
|
// stock Claude, whose auth is profiles alone and whose environment may not
|
|
@@ -259,7 +274,7 @@ function readClaudeAuth(
|
|
|
259
274
|
entry: AgentEntry,
|
|
260
275
|
raw: unknown,
|
|
261
276
|
authProfiles: ReadonlyMap<string, AuthProfile>,
|
|
262
|
-
):
|
|
277
|
+
): ReadClaudeAuth | { readonly problems: string[] } {
|
|
263
278
|
if (entry.baseURL !== undefined) {
|
|
264
279
|
if (raw === undefined) {
|
|
265
280
|
return {};
|
|
@@ -288,13 +303,21 @@ function readClaudeAuth(
|
|
|
288
303
|
|
|
289
304
|
const collected = collectClaudeAuth(raw, authProfiles, 'auth');
|
|
290
305
|
|
|
291
|
-
problems.push(...
|
|
306
|
+
problems.push(...collectSubscriptionProblems(entry));
|
|
292
307
|
|
|
293
|
-
if (
|
|
308
|
+
if (collected.auth === null) {
|
|
309
|
+
return { problems: [...collected.errors, ...problems] };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (problems.length > 0) {
|
|
294
313
|
return { problems };
|
|
295
314
|
}
|
|
296
315
|
|
|
297
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
auth: { profiles: collected.auth.profiles, placeholderEnv: {} },
|
|
318
|
+
...(collected.auth.mcpServers.length === 0 ? {} : { mcpServers: collected.auth.mcpServers }),
|
|
319
|
+
warnings: collected.errors,
|
|
320
|
+
};
|
|
298
321
|
}
|
|
299
322
|
|
|
300
323
|
// What a stock entry with `auth` sets that would override the subscription
|
|
@@ -2,14 +2,28 @@ import type { AuthProfile } from './collect-auth-profiles';
|
|
|
2
2
|
import { isRecord } from './report';
|
|
3
3
|
import { resolveAuthProfiles } from './resolve-auth-profiles';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* An MCP server a stock Claude session on a target that reaches impd's
|
|
7
|
+
* broker reaches over HTTP, with the header that one of the session's
|
|
8
|
+
* profiles sets for the server's host. The session sends a placeholder in
|
|
9
|
+
* that header, and impd swaps the credential in on the host's side.
|
|
10
|
+
*/
|
|
11
|
+
export interface ClaudeMCPServer {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly url: string;
|
|
14
|
+
readonly profile: string;
|
|
15
|
+
readonly header: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
/**
|
|
6
19
|
* The profiles a stock Claude session on a target that reaches impd's
|
|
7
20
|
* broker signs in through: one of them sends the subscription's setup
|
|
8
21
|
* token to the Anthropic API as a bearer authorization header, and the
|
|
9
|
-
* rest bind beside it, such as a GitHub token.
|
|
22
|
+
* rest bind beside it, such as a GitHub token or an MCP server's key.
|
|
10
23
|
*/
|
|
11
24
|
interface ClaudeAuth {
|
|
12
25
|
readonly profiles: readonly string[];
|
|
26
|
+
readonly mcpServers: readonly ClaudeMCPServer[];
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
interface CollectedClaudeAuth {
|
|
@@ -23,7 +37,8 @@ interface CollectedClaudeAuth {
|
|
|
23
37
|
* does not resolve, or whose profiles send no bearer authorization header
|
|
24
38
|
* to the Anthropic API, is left out with every problem that refused it, so
|
|
25
39
|
* stock Claude keeps the sign-in of the host it runs on rather than binding
|
|
26
|
-
* a credential the CLI would never send.
|
|
40
|
+
* a credential the CLI would never send. An MCP server that breaks a rule
|
|
41
|
+
* is left out alone, with an error, and the sign-in stays.
|
|
27
42
|
*/
|
|
28
43
|
export function collectClaudeAuth(
|
|
29
44
|
raw: unknown,
|
|
@@ -48,7 +63,7 @@ export function collectClaudeAuth(
|
|
|
48
63
|
};
|
|
49
64
|
}
|
|
50
65
|
|
|
51
|
-
const extra = Object.keys(raw).find((key) => key !== 'profiles');
|
|
66
|
+
const extra = Object.keys(raw).find((key) => key !== 'profiles' && key !== 'mcpServers');
|
|
52
67
|
|
|
53
68
|
if (extra !== undefined) {
|
|
54
69
|
return {
|
|
@@ -79,8 +94,106 @@ export function collectClaudeAuth(
|
|
|
79
94
|
};
|
|
80
95
|
}
|
|
81
96
|
|
|
82
|
-
|
|
97
|
+
const servers = collectMCPServers(raw['mcpServers'], field, selected, authProfiles);
|
|
98
|
+
|
|
99
|
+
return { auth: { profiles: selected, mcpServers: servers.servers }, errors: servers.errors };
|
|
83
100
|
}
|
|
84
101
|
|
|
85
102
|
// The one host Claude Code sends a subscription token to.
|
|
86
103
|
const ANTHROPIC_API_HOST = 'api.anthropic.com';
|
|
104
|
+
|
|
105
|
+
interface CollectedMCPServers {
|
|
106
|
+
readonly servers: readonly ClaudeMCPServer[];
|
|
107
|
+
readonly errors: readonly string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Reads the `mcpServers` map, server names to the server's URL and the
|
|
111
|
+
// selected profile that carries its credential.
|
|
112
|
+
function collectMCPServers(
|
|
113
|
+
raw: unknown,
|
|
114
|
+
field: string,
|
|
115
|
+
selected: readonly string[],
|
|
116
|
+
authProfiles: ReadonlyMap<string, AuthProfile>,
|
|
117
|
+
): CollectedMCPServers {
|
|
118
|
+
if (raw === undefined) {
|
|
119
|
+
return { servers: [], errors: [] };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!isRecord(raw) || Array.isArray(raw)) {
|
|
123
|
+
return { servers: [], errors: [`${field}.mcpServers must be an object of named servers`] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const servers: ClaudeMCPServer[] = [];
|
|
127
|
+
const errors: string[] = [];
|
|
128
|
+
|
|
129
|
+
for (const [name, entry] of Object.entries(raw)) {
|
|
130
|
+
const parsed = parseMCPServer(name, entry, field, selected, authProfiles);
|
|
131
|
+
|
|
132
|
+
if (typeof parsed === 'string') {
|
|
133
|
+
errors.push(`${field}.mcpServers.${name}: ${parsed}`);
|
|
134
|
+
} else {
|
|
135
|
+
servers.push(parsed);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { servers, errors };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Claude Code's rule for an MCP server's name.
|
|
143
|
+
const MCP_SERVER_NAME = /^[\w-]{1,64}$/u;
|
|
144
|
+
|
|
145
|
+
// The server an entry holds, or the first rule it breaks. The header comes
|
|
146
|
+
// from the profile, so the header the session sends and the one impd swaps
|
|
147
|
+
// the credential into never differ.
|
|
148
|
+
function parseMCPServer(
|
|
149
|
+
name: string,
|
|
150
|
+
entry: unknown,
|
|
151
|
+
field: string,
|
|
152
|
+
selected: readonly string[],
|
|
153
|
+
authProfiles: ReadonlyMap<string, AuthProfile>,
|
|
154
|
+
): ClaudeMCPServer | string {
|
|
155
|
+
if (!MCP_SERVER_NAME.test(name)) {
|
|
156
|
+
return 'a server name must be letters, digits, underscores or hyphens';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!isRecord(entry) || Array.isArray(entry)) {
|
|
160
|
+
return 'a server must be an object with url and profile';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const extra = Object.keys(entry).find((key) => key !== 'url' && key !== 'profile');
|
|
164
|
+
|
|
165
|
+
if (extra !== undefined) {
|
|
166
|
+
return `${extra} cannot be set: atc fixes the transport and the placeholder header`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const url = entry['url'];
|
|
170
|
+
const profileName = entry['profile'];
|
|
171
|
+
|
|
172
|
+
if (typeof profileName !== 'string' || !selected.includes(profileName)) {
|
|
173
|
+
return `profile must be one of ${field}.profiles`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const profile = authProfiles.get(profileName);
|
|
177
|
+
|
|
178
|
+
if (profile?.kind !== 'custom') {
|
|
179
|
+
return 'profile must be a custom profile, which sets one header for one host';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const parsedURL = typeof url === 'string' && URL.canParse(url) ? new URL(url) : null;
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
parsedURL === null ||
|
|
186
|
+
parsedURL.protocol !== 'https:' ||
|
|
187
|
+
parsedURL.port !== '' ||
|
|
188
|
+
parsedURL.username !== '' ||
|
|
189
|
+
parsedURL.password !== ''
|
|
190
|
+
) {
|
|
191
|
+
return 'url must be an https URL with no port and no user info';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (parsedURL.hostname !== profile.host) {
|
|
195
|
+
return `url must be on ${profile.host}, the host profile ${profileName} sends its credential to`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { name, url: parsedURL.href, profile: profileName, header: profile.header };
|
|
199
|
+
}
|
|
@@ -51,6 +51,9 @@ export function collectLegacyAgents(
|
|
|
51
51
|
...(claudeAuth.auth === null
|
|
52
52
|
? {}
|
|
53
53
|
: { auth: { profiles: claudeAuth.auth.profiles, placeholderEnv: {} } }),
|
|
54
|
+
...(claudeAuth.auth === null || claudeAuth.auth.mcpServers.length === 0
|
|
55
|
+
? {}
|
|
56
|
+
: { mcpServers: claudeAuth.auth.mcpServers }),
|
|
54
57
|
},
|
|
55
58
|
{
|
|
56
59
|
id: 'grok',
|