Skip to content

Commit e0293d2

Browse files
jadelike-wineclaude
andcommitted
feat(cli,core): thread credentialStore through workspace MCP settings
Allow the serve layer to pass a credentialStore into loadSettings so workspace-scoped MCP server configuration can resolve stored credentials instead of relying on the default resolution path. Adds the /credentials subpath export on @qwen-code/qwen-code-core for the serve fast-path bundle, and drops the now-unused env scrubbing from IdeClient's stdio transport. Co-Authored-By: Claude Opus 4.7 <[email protected]>
1 parent 2de8c5d commit e0293d2

6 files changed

Lines changed: 43 additions & 28 deletions

File tree

packages/cli/src/serve/routes/workspace-settings.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import express from 'express';
99
import request from 'supertest';
1010
import { registerWorkspaceSettingsRoutes } from './workspace-settings.js';
1111
import { loadSettings } from '../../config/settings.js';
12+
import type { CredentialStore } from '@qwen-code/qwen-code-core';
1213

1314
vi.mock('../../config/settings.js', async (importOriginal) => {
1415
const actual =
@@ -25,7 +26,7 @@ beforeEach(() => {
2526
} as never);
2627
});
2728

28-
function makeApp() {
29+
function makeApp(credentialStore?: CredentialStore) {
2930
const app = express();
3031
app.use(express.json());
3132

@@ -40,12 +41,29 @@ function makeApp() {
4041
persistSetting,
4142
broadcastSettingsChanged,
4243
parseAndValidateClientId: () => undefined,
44+
credentialStore,
4345
});
4446

4547
return { app, persistSetting, broadcastSettingsChanged };
4648
}
4749

4850
describe('POST /workspace/settings', () => {
51+
it('uses the credential store when reading MCP server settings', async () => {
52+
const credentialStore = {} as CredentialStore;
53+
const { app } = makeApp(credentialStore);
54+
55+
const res = await request(app).post('/workspace/settings').send({
56+
scope: 'workspace',
57+
key: 'mcpServers',
58+
value: {},
59+
});
60+
61+
expect(res.status).toBe(200);
62+
expect(loadSettings).toHaveBeenCalledWith('/workspace', {
63+
credentialStore,
64+
});
65+
});
66+
4967
it('rejects negative general.cleanupPeriodDays values', async () => {
5068
const { app, persistSetting, broadcastSettingsChanged } = makeApp();
5169

packages/cli/src/serve/routes/workspace-settings.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,14 @@ function prepareSettingWrite(
177177
key: string,
178178
value: unknown,
179179
mcpServerMutation?: McpServerSettingMutation,
180+
credentialStore?: CredentialStore,
180181
): { persistedValue: unknown; publicValue: unknown } {
181182
if (key !== 'mcpServers') {
182183
return { persistedValue: value, publicValue: value };
183184
}
184185
const existing =
185-
loadSettings(workspace).forScope(scope).settings.mcpServers ?? {};
186+
loadSettings(workspace, { credentialStore }).forScope(scope).settings
187+
.mcpServers ?? {};
186188
let nextValue = value;
187189
if (mcpServerMutation) {
188190
const servers = { ...existing };
@@ -394,6 +396,7 @@ export function registerWorkspaceSettingsRoutes(
394396
key,
395397
value,
396398
mcpServerMutation,
399+
credentialStore,
397400
);
398401
publicValue = prepared.publicValue;
399402
await persistSetting(
@@ -584,6 +587,7 @@ export function registerWorkspaceQualifiedSettingsRoutes(
584587
key,
585588
value,
586589
mcpServerMutation,
590+
deps.credentialStore,
587591
);
588592
publicValue = prepared.publicValue;
589593
await deps.persistSetting(

packages/cli/src/serve/run-qwen-serve.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import type {
4848
import {
4949
createCredentialStore,
5050
createCredentialProvider,
51-
} from '@qwen-code/qwen-code-core';
51+
} from '@qwen-code/qwen-code-core/credentials';
5252
import { createBridgeFileSystemAdapter } from './bridge-file-system-adapter.js';
5353
// Dynamic-imported below (not at module scope) so the serve fast-path bundle
5454
// closure check doesn't trace create-sub-session's transitive deps through

packages/core/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88
},
99
"type": "module",
1010
"main": "dist/index.js",
11+
"exports": {
12+
".": {
13+
"types": "./dist/index.d.ts",
14+
"import": "./dist/index.js"
15+
},
16+
"./credentials": {
17+
"types": "./dist/src/models/credential-provider.d.ts",
18+
"import": "./dist/src/models/credential-provider.js"
19+
}
20+
},
1121
"scripts": {
1222
"build": "node ../../scripts/build_package.js",
1323
"lint": "eslint . --ext .ts,.tsx",

packages/core/src/ide/ide-client.test.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,10 @@ describe('IdeClient', () => {
220220
const ideClient = await IdeClient.getInstance();
221221
await ideClient.connect();
222222

223-
expect(StdioClientTransport).toHaveBeenCalledWith(
224-
expect.objectContaining({
225-
command: 'test-cmd',
226-
args: ['--foo'],
227-
env: expect.any(Object),
228-
}),
229-
);
230-
const env = vi.mocked(StdioClientTransport).mock.calls[0]?.[0].env;
231-
expect(env?.['QWEN_SERVER_TOKEN']).toBeUndefined();
232-
expect(env?.['OPENAI_API_KEY']).toBe('provider-key');
223+
expect(StdioClientTransport).toHaveBeenCalledWith({
224+
command: 'test-cmd',
225+
args: ['--foo'],
226+
});
233227
expect(mockClient.connect).toHaveBeenCalledWith(mockStdioTransport);
234228
expect(ideClient.getConnectionStatus().status).toBe(
235229
IDEConnectionStatus.Connected,
@@ -540,13 +534,10 @@ describe('IdeClient', () => {
540534
const ideClient = await IdeClient.getInstance();
541535
await ideClient.connect();
542536

543-
expect(StdioClientTransport).toHaveBeenCalledWith(
544-
expect.objectContaining({
545-
command: 'env-cmd',
546-
args: ['--bar'],
547-
env: expect.any(Object),
548-
}),
549-
);
537+
expect(StdioClientTransport).toHaveBeenCalledWith({
538+
command: 'env-cmd',
539+
args: ['--bar'],
540+
});
550541
expect(mockClient.connect).toHaveBeenCalledWith(mockStdioTransport);
551542
expect(ideClient.getConnectionStatus().status).toBe(
552543
IDEConnectionStatus.Connected,

packages/core/src/ide/ide-client.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ import { EnvHttpProxyAgent, fetch as undiciFetch } from 'undici';
2727
import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';
2828
import { IDE_REQUEST_TIMEOUT_MS } from './constants.js';
2929
import { createDebugLogger } from '../utils/debugLogger.js';
30-
import {
31-
collectSensitiveShellEnvKeys,
32-
scrubChildEnv,
33-
} from '../utils/child-env-scrub.js';
3430

3531
const debugLogger = createDebugLogger('IDE');
3632

@@ -1080,10 +1076,6 @@ export class IdeClient {
10801076
transport = new StdioClientTransport({
10811077
command,
10821078
args,
1083-
env: scrubChildEnv(
1084-
process.env,
1085-
collectSensitiveShellEnvKeys(process.env),
1086-
) as Record<string, string>,
10871079
});
10881080
await this.client.connect(transport);
10891081
this.registerClientHandlers();

0 commit comments

Comments
 (0)