Skip to content

Commit 43f00df

Browse files
David RuzickaAgent
andcommitted
fix: collect env vars from upstream_mcp auth in profile index
upstream_mcp[*].auth.value_from_env was ignored by extractEnvVars, so env vars like YOUTRACK_TOKEN never appeared in the HTML profile index or local stdio snippets. Co-authored-by: Agent <noreply@agent>
1 parent 7ca1d16 commit 43f00df

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

src/profile/profile-resolver.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,124 @@ describe('profile-resolver', () => {
7474
await expect(resolveProfileById('missing', profilesDir)).rejects.toThrow('openapi_spec_path');
7575
});
7676

77+
it('collects env vars from upstream_mcp bearer auth', async () => {
78+
const root = await createTempDir();
79+
const profilesDir = path.join(root, 'profiles');
80+
81+
await writeJson(path.join(profilesDir, 'proxy-auth.json'), {
82+
profile_name: 'proxy-auth-profile',
83+
profile_id: 'proxy-auth',
84+
tools: [],
85+
upstream_mcp: [
86+
{
87+
name: 'youtrack',
88+
transport: { type: 'http-streamable', url: 'https://youtrack.example.com/mcp' },
89+
auth: { type: 'bearer', value_from_env: 'YOUTRACK_TOKEN' },
90+
},
91+
],
92+
});
93+
94+
const profiles = await listProfilesDetailed(profilesDir);
95+
expect(profiles).toHaveLength(1);
96+
expect(profiles[0].envVars).toEqual(['YOUTRACK_TOKEN']);
97+
expect(profiles[0].authMethods).toEqual([]);
98+
});
99+
100+
it('collects env vars from multiple upstream_mcp entries with different auth types', async () => {
101+
const root = await createTempDir();
102+
const profilesDir = path.join(root, 'profiles');
103+
104+
await writeJson(path.join(profilesDir, 'multi-upstream.json'), {
105+
profile_name: 'multi-upstream',
106+
profile_id: 'multi-upstream',
107+
tools: [],
108+
upstream_mcp: [
109+
{
110+
name: 'svc-a',
111+
transport: { type: 'http-streamable', url: 'https://svc-a.example.com/mcp' },
112+
auth: { type: 'bearer', value_from_env: 'SVC_A_TOKEN' },
113+
},
114+
{
115+
name: 'svc-b',
116+
transport: { type: 'http-streamable', url: 'https://svc-b.example.com/mcp' },
117+
auth: { type: 'custom-header', header_name: 'X-Api-Key', value_from_env: 'SVC_B_KEY' },
118+
},
119+
{
120+
name: 'svc-c',
121+
transport: { type: 'http-streamable', url: 'https://svc-c.example.com/mcp' },
122+
},
123+
],
124+
});
125+
126+
const profiles = await listProfilesDetailed(profilesDir);
127+
expect(profiles).toHaveLength(1);
128+
expect(profiles[0].envVars).toEqual(['SVC_A_TOKEN', 'SVC_B_KEY']);
129+
expect(profiles[0].authMethods).toEqual([]);
130+
});
131+
132+
it('collects env vars from upstream_mcp session-cookie auth', async () => {
133+
const root = await createTempDir();
134+
const profilesDir = path.join(root, 'profiles');
135+
136+
await writeJson(path.join(profilesDir, 'upstream-cookie.json'), {
137+
profile_name: 'upstream-cookie',
138+
profile_id: 'upstream-cookie',
139+
tools: [],
140+
upstream_mcp: [
141+
{
142+
name: 'legacy',
143+
transport: { type: 'http-streamable', url: 'https://legacy.example.com/mcp' },
144+
auth: {
145+
type: 'session-cookie',
146+
session_cookie_config: {
147+
login_endpoint: '/login',
148+
login_method: 'POST',
149+
login_content_type: 'application/json',
150+
username_field: 'user',
151+
username_from_env: 'LEGACY_USER',
152+
password_field: 'pass',
153+
password_from_env: 'LEGACY_PASS',
154+
cookie_names: ['sid'],
155+
},
156+
},
157+
},
158+
],
159+
});
160+
161+
const profiles = await listProfilesDetailed(profilesDir);
162+
expect(profiles).toHaveLength(1);
163+
expect(profiles[0].envVars).toEqual(['LEGACY_PASS', 'LEGACY_USER']);
164+
expect(profiles[0].authMethods).toEqual([]);
165+
});
166+
167+
it('merges env vars from both interceptors.auth and upstream_mcp auth without duplication', async () => {
168+
const root = await createTempDir();
169+
const profilesDir = path.join(root, 'profiles');
170+
171+
await writeJson(path.join(profilesDir, 'combined.json'), {
172+
profile_name: 'combined',
173+
profile_id: 'combined',
174+
tools: [],
175+
interceptors: {
176+
auth: { type: 'bearer', value_from_env: 'CLIENT_TOKEN' },
177+
},
178+
upstream_mcp: [
179+
{
180+
name: 'upstream',
181+
transport: { type: 'http-streamable', url: 'https://upstream.example.com/mcp' },
182+
auth: { type: 'bearer', value_from_env: 'UPSTREAM_TOKEN' },
183+
},
184+
],
185+
});
186+
187+
const profiles = await listProfilesDetailed(profilesDir);
188+
expect(profiles).toHaveLength(1);
189+
expect(profiles[0].envVars).toEqual(['CLIENT_TOKEN', 'UPSTREAM_TOKEN']);
190+
expect(profiles[0].authMethods).toEqual([
191+
{ type: 'bearer', headerName: undefined, queryParam: undefined, valueFromEnv: 'CLIENT_TOKEN' },
192+
]);
193+
});
194+
77195
it('returns specPath=undefined for upstream_mcp proxy profile with no openapi_spec_path', async () => {
78196
const root = await createTempDir();
79197
const profilesDir = path.join(root, 'profiles');

src/profile/profile-resolver.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,18 @@ function extractEnvVars(profile: Record<string, unknown>): string[] {
183183
}
184184
}
185185

186+
const upstreamMcp = profile.upstream_mcp;
187+
if (Array.isArray(upstreamMcp)) {
188+
for (const upstream of upstreamMcp) {
189+
if (!upstream || typeof upstream !== 'object') continue;
190+
const upstreamRecord = upstream as Record<string, unknown>;
191+
const auth = upstreamRecord.auth;
192+
if (auth && typeof auth === 'object') {
193+
collectEnvVarsFromAuth(auth as Record<string, unknown>, envVars);
194+
}
195+
}
196+
}
197+
186198
return Array.from(envVars).sort((a, b) => a.localeCompare(b));
187199
}
188200

0 commit comments

Comments
 (0)