mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 07:10:55 +00:00
fix(acp): support SSE and HTTP MCP servers in ACP mode (#3574)
In ACP mode, the Mcp server list sent by the IDE client can include
SSE (type: "sse") and HTTP (type: "http") transports, but the previous
implementation only handled stdio servers via toStdioServer(). Non-stdio
servers were silently skipped (continue), so any SSE/HTTP-configured
MCP server would never be registered.
Changes:
- Add toSseServer() helper: detects type=="sse" servers and maps them
to MCPServerConfig(url=..., headers=...)
- Add toHttpServer() helper: detects type=="http" servers and maps them
to MCPServerConfig(httpUrl=..., headers=...)
- Refactor newSessionConfig() loop to handle all three transport types
- Declare mcpCapabilities: { sse: true, http: true } in agentCapabilities
so IDE clients know this agent supports these transports without needing
a transparent proxy
- Export the three helper functions for unit testing
Tests:
- Unit tests for toStdioServer / toSseServer / toHttpServer helpers
(type discrimination, mutual exclusion)
- Integration-style tests for QwenAgent.initialize() mcpCapabilities
- Integration-style tests for newSession() with SSE/HTTP MCP servers,
verifying MCPServerConfig is constructed with the correct arguments
(url vs httpUrl, headers passthrough, empty-headers → undefined)
Fixes #3472
This commit is contained in:
parent
5c1e636dbe
commit
97926a07fe
2 changed files with 492 additions and 17 deletions
|
|
@ -45,7 +45,22 @@ vi.mock('@agentclientprotocol/sdk', () => ({
|
|||
},
|
||||
})),
|
||||
ndJsonStream: vi.fn().mockReturnValue({}),
|
||||
RequestError: class RequestError extends Error {},
|
||||
RequestError: class RequestError extends Error {
|
||||
static authRequired = vi
|
||||
.fn()
|
||||
.mockImplementation((data: unknown, msg: string) => {
|
||||
const err = new Error(msg);
|
||||
Object.assign(err, data);
|
||||
return err;
|
||||
});
|
||||
static invalidParams = vi
|
||||
.fn()
|
||||
.mockImplementation((data: unknown, msg: string) => {
|
||||
const err = new Error(msg);
|
||||
Object.assign(err, data);
|
||||
return err;
|
||||
});
|
||||
},
|
||||
PROTOCOL_VERSION: '1.0.0',
|
||||
}));
|
||||
|
||||
|
|
@ -73,7 +88,9 @@ vi.mock('@qwen-code/qwen-code-core', () => ({
|
|||
clearCachedCredentialFile: vi.fn(),
|
||||
QwenOAuth2Event: {},
|
||||
qwenOAuth2Events: { on: vi.fn(), off: vi.fn() },
|
||||
MCPServerConfig: {},
|
||||
MCPServerConfig: vi.fn().mockImplementation((...args: unknown[]) => ({
|
||||
_args: args,
|
||||
})),
|
||||
SessionService: vi.fn(),
|
||||
tokenLimit: vi.fn(),
|
||||
SessionStartSource: {
|
||||
|
|
@ -90,18 +107,31 @@ vi.mock('./authMethods.js', () => ({ buildAuthMethods: vi.fn() }));
|
|||
vi.mock('./service/filesystem.js', () => ({
|
||||
AcpFileSystemService: vi.fn(),
|
||||
}));
|
||||
vi.mock('../config/settings.js', () => ({ SettingScope: {} }));
|
||||
vi.mock('../config/settings.js', () => ({
|
||||
SettingScope: {},
|
||||
loadSettings: vi.fn(),
|
||||
}));
|
||||
vi.mock('../config/config.js', () => ({ loadCliConfig: vi.fn() }));
|
||||
vi.mock('./session/Session.js', () => ({ Session: vi.fn() }));
|
||||
vi.mock('../utils/acpModelUtils.js', () => ({
|
||||
formatAcpModelId: vi.fn(),
|
||||
}));
|
||||
|
||||
import { runAcpAgent } from './acpAgent.js';
|
||||
import {
|
||||
runAcpAgent,
|
||||
toStdioServer,
|
||||
toSseServer,
|
||||
toHttpServer,
|
||||
} from './acpAgent.js';
|
||||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
import type { CliArgs } from '../config/config.js';
|
||||
import { SessionEndReason } from '@qwen-code/qwen-code-core';
|
||||
import { SessionEndReason, MCPServerConfig } from '@qwen-code/qwen-code-core';
|
||||
import type { McpServer } from '@agentclientprotocol/sdk';
|
||||
import { AgentSideConnection } from '@agentclientprotocol/sdk';
|
||||
import { loadSettings } from '../config/settings.js';
|
||||
import { loadCliConfig } from '../config/config.js';
|
||||
import { Session } from './session/Session.js';
|
||||
|
||||
describe('runAcpAgent shutdown cleanup', () => {
|
||||
let processExitSpy: MockInstance<typeof process.exit>;
|
||||
|
|
@ -480,3 +510,387 @@ describe('runAcpAgent SessionEnd hooks', () => {
|
|||
expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unit tests for toStdioServer / toSseServer / toHttpServer helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('toStdioServer', () => {
|
||||
const stdioServer = {
|
||||
name: 'my-stdio',
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
env: [],
|
||||
} as unknown as McpServer;
|
||||
|
||||
const sseServer = {
|
||||
type: 'sse',
|
||||
name: 'my-sse',
|
||||
url: 'http://localhost:3000/sse',
|
||||
headers: [],
|
||||
} as unknown as McpServer;
|
||||
|
||||
it('returns the server when it is a stdio server', () => {
|
||||
expect(toStdioServer(stdioServer)).toBe(stdioServer);
|
||||
});
|
||||
|
||||
it('returns undefined for SSE server', () => {
|
||||
expect(toStdioServer(sseServer)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for HTTP server', () => {
|
||||
const httpServer = {
|
||||
type: 'http',
|
||||
name: 'my-http',
|
||||
url: 'http://localhost:3000/mcp',
|
||||
headers: [],
|
||||
} as unknown as McpServer;
|
||||
expect(toStdioServer(httpServer)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toSseServer', () => {
|
||||
it('returns the server when type is sse', () => {
|
||||
const sseServer = {
|
||||
type: 'sse',
|
||||
name: 'my-sse',
|
||||
url: 'http://localhost:3000/sse',
|
||||
headers: [],
|
||||
} as unknown as McpServer;
|
||||
const result = toSseServer(sseServer);
|
||||
expect(result).toBe(sseServer);
|
||||
expect(result?.type).toBe('sse');
|
||||
});
|
||||
|
||||
it('returns undefined for stdio server', () => {
|
||||
const stdioServer = {
|
||||
name: 'my-stdio',
|
||||
command: 'node',
|
||||
args: [],
|
||||
env: [],
|
||||
} as unknown as McpServer;
|
||||
expect(toSseServer(stdioServer)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for http server', () => {
|
||||
const httpServer = {
|
||||
type: 'http',
|
||||
name: 'my-http',
|
||||
url: 'http://localhost:3000/mcp',
|
||||
headers: [],
|
||||
} as unknown as McpServer;
|
||||
expect(toSseServer(httpServer)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toHttpServer', () => {
|
||||
it('returns the server when type is http', () => {
|
||||
const httpServer = {
|
||||
type: 'http',
|
||||
name: 'my-http',
|
||||
url: 'http://localhost:3000/mcp',
|
||||
headers: [],
|
||||
} as unknown as McpServer;
|
||||
const result = toHttpServer(httpServer);
|
||||
expect(result).toBe(httpServer);
|
||||
expect(result?.type).toBe('http');
|
||||
});
|
||||
|
||||
it('returns undefined for stdio server', () => {
|
||||
const stdioServer = {
|
||||
name: 'my-stdio',
|
||||
command: 'node',
|
||||
args: [],
|
||||
env: [],
|
||||
} as unknown as McpServer;
|
||||
expect(toHttpServer(stdioServer)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for sse server', () => {
|
||||
const sseServer = {
|
||||
type: 'sse',
|
||||
name: 'my-sse',
|
||||
url: 'http://localhost:3000/sse',
|
||||
headers: [],
|
||||
} as unknown as McpServer;
|
||||
expect(toHttpServer(sseServer)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests for QwenAgent.initialize() mcpCapabilities + newSession SSE/HTTP
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('QwenAgent MCP SSE/HTTP support', () => {
|
||||
// We need to capture the agent factory from AgentSideConnection constructor
|
||||
let capturedAgentFactory:
|
||||
| ((conn: AgentSideConnectionLike) => AgentLike)
|
||||
| undefined;
|
||||
|
||||
type AgentSideConnectionLike = { closed: Promise<void> };
|
||||
type AgentLike = {
|
||||
initialize: (args: Record<string, unknown>) => Promise<unknown>;
|
||||
newSession: (args: Record<string, unknown>) => Promise<unknown>;
|
||||
};
|
||||
|
||||
let mockConfig: Config;
|
||||
let processExitSpy: MockInstance<typeof process.exit>;
|
||||
let stdinDestroySpy: MockInstance<typeof process.stdin.destroy>;
|
||||
let stdoutDestroySpy: MockInstance<typeof process.stdout.destroy>;
|
||||
|
||||
const mockArgv = {} as CliArgs;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockConnectionState.reset();
|
||||
capturedAgentFactory = undefined;
|
||||
|
||||
// Override AgentSideConnection mock to capture factory
|
||||
vi.mocked(AgentSideConnection).mockImplementation((factory: unknown) => {
|
||||
capturedAgentFactory = factory as typeof capturedAgentFactory;
|
||||
return {
|
||||
get closed() {
|
||||
return mockConnectionState.promise;
|
||||
},
|
||||
} as unknown as InstanceType<typeof AgentSideConnection>;
|
||||
});
|
||||
|
||||
mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getHookSystem: vi.fn().mockReturnValue(undefined),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(false),
|
||||
hasHooksForEvent: vi.fn().mockReturnValue(false),
|
||||
getModel: vi.fn().mockReturnValue('test-model'),
|
||||
getModelsConfig: vi.fn().mockReturnValue({
|
||||
getCurrentAuthType: vi.fn().mockReturnValue('api-key'),
|
||||
}),
|
||||
refreshAuth: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((() => undefined) as unknown as typeof process.exit);
|
||||
stdinDestroySpy = vi
|
||||
.spyOn(process.stdin, 'destroy')
|
||||
.mockImplementation(() => process.stdin);
|
||||
stdoutDestroySpy = vi
|
||||
.spyOn(process.stdout, 'destroy')
|
||||
.mockImplementation(() => process.stdout);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
processExitSpy.mockRestore();
|
||||
stdinDestroySpy.mockRestore();
|
||||
stdoutDestroySpy.mockRestore();
|
||||
});
|
||||
|
||||
it('initialize response includes mcpCapabilities with sse and http', async () => {
|
||||
const mockSettings = {
|
||||
merged: { mcpServers: {} },
|
||||
} as unknown as LoadedSettings;
|
||||
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
|
||||
|
||||
await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined());
|
||||
|
||||
const fakeConn = {
|
||||
get closed() {
|
||||
return mockConnectionState.promise;
|
||||
},
|
||||
} as AgentSideConnectionLike;
|
||||
|
||||
const agent = capturedAgentFactory!(fakeConn) as AgentLike;
|
||||
const response = await agent.initialize({ clientCapabilities: {} });
|
||||
|
||||
expect(response).toMatchObject({
|
||||
agentCapabilities: {
|
||||
mcpCapabilities: {
|
||||
sse: true,
|
||||
http: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockConnectionState.resolve();
|
||||
await agentPromise;
|
||||
});
|
||||
|
||||
function makeInnerConfig() {
|
||||
return {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getModelsConfig: vi.fn().mockReturnValue({
|
||||
getCurrentAuthType: vi.fn().mockReturnValue('api-key'),
|
||||
}),
|
||||
refreshAuth: vi.fn().mockResolvedValue(undefined),
|
||||
getModel: vi.fn().mockReturnValue('m'),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({}),
|
||||
getAvailableModels: vi.fn().mockReturnValue([]),
|
||||
getModes: vi.fn().mockReturnValue([]),
|
||||
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getAuthType: vi.fn().mockReturnValue('api-key'),
|
||||
getAllConfiguredModels: vi.fn().mockReturnValue([]),
|
||||
getGeminiClient: vi.fn().mockReturnValue({
|
||||
isInitialized: vi.fn().mockReturnValue(true),
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
getFileSystemService: vi.fn().mockReturnValue(undefined),
|
||||
setFileSystemService: vi.fn(),
|
||||
getHookSystem: vi.fn().mockReturnValue(undefined),
|
||||
getDisableAllHooks: vi.fn().mockReturnValue(true),
|
||||
hasHooksForEvent: vi.fn().mockReturnValue(false),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSessionSettings() {
|
||||
return {
|
||||
merged: { mcpServers: {} },
|
||||
getUserHooks: vi.fn().mockReturnValue({}),
|
||||
getProjectHooks: vi.fn().mockReturnValue({}),
|
||||
} as unknown as LoadedSettings;
|
||||
}
|
||||
|
||||
async function setupSessionMocks(sessionId: string) {
|
||||
const innerConfig = makeInnerConfig();
|
||||
vi.mocked(loadSettings).mockReturnValue(makeSessionSettings());
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(
|
||||
innerConfig as unknown as Config,
|
||||
);
|
||||
vi.mocked(Session).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
getId: vi.fn().mockReturnValue(sessionId),
|
||||
getConfig: vi.fn().mockReturnValue(innerConfig),
|
||||
sendAvailableCommandsUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
replayHistory: vi.fn().mockResolvedValue(undefined),
|
||||
installRewriter: vi.fn(),
|
||||
}) as unknown as InstanceType<typeof Session>,
|
||||
);
|
||||
return innerConfig;
|
||||
}
|
||||
|
||||
it('newSession with SSE MCP server creates MCPServerConfig with url', async () => {
|
||||
await setupSessionMocks('session-sse');
|
||||
|
||||
const agentPromise = runAcpAgent(
|
||||
mockConfig,
|
||||
makeSessionSettings(),
|
||||
mockArgv,
|
||||
);
|
||||
await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined());
|
||||
|
||||
const agent = capturedAgentFactory!({
|
||||
get closed() {
|
||||
return mockConnectionState.promise;
|
||||
},
|
||||
}) as AgentLike;
|
||||
|
||||
await agent.newSession({
|
||||
cwd: '/tmp',
|
||||
mcpServers: [
|
||||
{
|
||||
type: 'sse',
|
||||
name: 'my-sse-server',
|
||||
url: 'http://localhost:3001/sse',
|
||||
headers: [{ name: 'Authorization', value: 'Bearer token123' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(MCPServerConfig).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'http://localhost:3001/sse',
|
||||
undefined,
|
||||
{ Authorization: 'Bearer token123' },
|
||||
);
|
||||
|
||||
mockConnectionState.resolve();
|
||||
await agentPromise;
|
||||
});
|
||||
|
||||
it('newSession with HTTP MCP server creates MCPServerConfig with httpUrl', async () => {
|
||||
await setupSessionMocks('session-http');
|
||||
|
||||
const agentPromise = runAcpAgent(
|
||||
mockConfig,
|
||||
makeSessionSettings(),
|
||||
mockArgv,
|
||||
);
|
||||
await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined());
|
||||
|
||||
const agent = capturedAgentFactory!({
|
||||
get closed() {
|
||||
return mockConnectionState.promise;
|
||||
},
|
||||
}) as AgentLike;
|
||||
|
||||
await agent.newSession({
|
||||
cwd: '/tmp',
|
||||
mcpServers: [
|
||||
{
|
||||
type: 'http',
|
||||
name: 'my-http-server',
|
||||
url: 'http://localhost:3002/mcp',
|
||||
headers: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(MCPServerConfig).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'http://localhost:3002/mcp',
|
||||
undefined,
|
||||
);
|
||||
|
||||
mockConnectionState.resolve();
|
||||
await agentPromise;
|
||||
});
|
||||
|
||||
it('newSession with SSE MCP server and empty headers passes undefined for headers', async () => {
|
||||
await setupSessionMocks('session-sse-noheaders');
|
||||
|
||||
const agentPromise = runAcpAgent(
|
||||
mockConfig,
|
||||
makeSessionSettings(),
|
||||
mockArgv,
|
||||
);
|
||||
await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined());
|
||||
|
||||
const agent = capturedAgentFactory!({
|
||||
get closed() {
|
||||
return mockConnectionState.promise;
|
||||
},
|
||||
}) as AgentLike;
|
||||
|
||||
await agent.newSession({
|
||||
cwd: '/tmp',
|
||||
mcpServers: [
|
||||
{
|
||||
type: 'sse',
|
||||
name: 'no-header-sse',
|
||||
url: 'http://localhost:3003/sse',
|
||||
headers: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(MCPServerConfig).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'http://localhost:3003/sse',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
mockConnectionState.resolve();
|
||||
await agentPromise;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ import type {
|
|||
LoadSessionRequest,
|
||||
LoadSessionResponse,
|
||||
McpServer,
|
||||
McpServerHttp,
|
||||
McpServerSse,
|
||||
McpServerStdio,
|
||||
NewSessionRequest,
|
||||
NewSessionResponse,
|
||||
|
|
@ -162,13 +164,31 @@ export async function runAcpAgent(
|
|||
process.off('SIGINT', shutdownHandler);
|
||||
}
|
||||
|
||||
function toStdioServer(server: McpServer): McpServerStdio | undefined {
|
||||
export function toStdioServer(server: McpServer): McpServerStdio | undefined {
|
||||
if ('command' in server && 'args' in server && 'env' in server) {
|
||||
return server as McpServerStdio;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function toSseServer(
|
||||
server: McpServer,
|
||||
): (McpServerSse & { type: 'sse' }) | undefined {
|
||||
if ('type' in server && server.type === 'sse') {
|
||||
return server as McpServerSse & { type: 'sse' };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function toHttpServer(
|
||||
server: McpServer,
|
||||
): (McpServerHttp & { type: 'http' }) | undefined {
|
||||
if ('type' in server && server.type === 'http') {
|
||||
return server as McpServerHttp & { type: 'http' };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
class QwenAgent implements Agent {
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
private clientCapabilities: ClientCapabilities | undefined;
|
||||
|
|
@ -204,6 +224,10 @@ class QwenAgent implements Agent {
|
|||
list: {},
|
||||
resume: {},
|
||||
},
|
||||
mcpCapabilities: {
|
||||
sse: true,
|
||||
http: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -499,18 +523,55 @@ class QwenAgent implements Agent {
|
|||
|
||||
for (const server of mcpServers) {
|
||||
const stdioServer = toStdioServer(server);
|
||||
if (!stdioServer) continue;
|
||||
|
||||
const env: Record<string, string> = {};
|
||||
for (const { name: envName, value } of stdioServer.env) {
|
||||
env[envName] = value;
|
||||
if (stdioServer) {
|
||||
const env: Record<string, string> = {};
|
||||
for (const { name: envName, value } of stdioServer.env) {
|
||||
env[envName] = value;
|
||||
}
|
||||
mergedMcpServers[stdioServer.name] = new MCPServerConfig(
|
||||
stdioServer.command,
|
||||
stdioServer.args,
|
||||
env,
|
||||
cwd,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const sseServer = toSseServer(server);
|
||||
if (sseServer) {
|
||||
const headers: Record<string, string> = {};
|
||||
for (const { name: headerName, value } of sseServer.headers) {
|
||||
headers[headerName] = value;
|
||||
}
|
||||
mergedMcpServers[sseServer.name] = new MCPServerConfig(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
sseServer.url,
|
||||
undefined,
|
||||
Object.keys(headers).length > 0 ? headers : undefined,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const httpServer = toHttpServer(server);
|
||||
if (httpServer) {
|
||||
const headers: Record<string, string> = {};
|
||||
for (const { name: headerName, value } of httpServer.headers) {
|
||||
headers[headerName] = value;
|
||||
}
|
||||
mergedMcpServers[httpServer.name] = new MCPServerConfig(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
httpServer.url,
|
||||
Object.keys(headers).length > 0 ? headers : undefined,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
mergedMcpServers[stdioServer.name] = new MCPServerConfig(
|
||||
stdioServer.command,
|
||||
stdioServer.args,
|
||||
env,
|
||||
cwd,
|
||||
);
|
||||
}
|
||||
|
||||
const settings = { ...this.settings.merged, mcpServers: mergedMcpServers };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue