Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcp-oauth-authenticate-tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix the missing OAuth authenticate tool for remote MCP servers that require login.
Original file line number Diff line number Diff line change
Expand Up @@ -156,24 +156,33 @@ export class WorkspaceMcpService extends Disposable implements IWorkspaceMcpServ
ready: Promise<void>,
extra?: readonly string[],
): (name: string) => boolean {
const baseline = new Set<string>(extra);
for (const entry of view.list()) {
baseline.add(entry.name);
}
let baseline: Set<string> | undefined;
let frozen = false;
const snapshot = (): Set<string> => {
if (baseline === undefined) {
baseline = new Set<string>(extra);
for (const entry of view.list()) {
baseline.add(entry.name);
}
}
return baseline;
};
void ready.then(
() => {
snapshot();
frozen = true;
},
() => {
snapshot();
frozen = true;
},
);
return (name) => {
if (baseline.has(name)) return true;
const names = snapshot();
if (names.has(name)) return true;
if (frozen) return false;
if (view.get(name) === undefined) return false;
baseline.add(name);
names.add(name);
return true;
};
}
Expand Down
28 changes: 28 additions & 0 deletions packages/agent-core-v2/test/agent/mcp/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,34 @@ describe('AgentMcpService', () => {
]);
});

it('registers the synthetic authenticate tool for a server that settled needs-auth before attach', () => {
const oauthService = {
beginAuthorization: async () => ({
authorizationUrl: new URL('https://example.com/authorize'),
complete: async () => {},
cancel: async () => {},
}),
} as unknown as McpOAuthService;
const manager = new FakeMcpManager({ oauthService });
manager.needsAuth();

createService(manager);

const tools = ix.get(IAgentToolRegistryService).list();
expect(tools).toEqual([
expect.objectContaining({
name: 'mcp__needs-auth__authenticate',
source: 'mcp',
}),
]);
expect(events).toContainEqual(
expect.objectContaining({
type: 'mcp.server.status',
server: expect.objectContaining({ name: 'needs-auth', status: 'needs-auth' }),
}),
);
});

it('keeps tools registered when a connected server fails so later calls can heal', async () => {
const manager = new FakeMcpManager();
const client = fakeMcpClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { IWorkspaceMcpService, type ISessionMcpOverlay } from '#/workspace/works
import { WorkspaceMcpService } from '#/workspace/workspaceMcp/workspaceMcpService';

import { stubLog } from '../../_base/log/stubs';
import { createMemoryMcpOAuthStore, stdioFixture } from '../../mcpCore/stubs';
import { createMemoryMcpOAuthStore, startInProcessHttpMcpServer, stdioFixture } from '../../mcpCore/stubs';
import { registerAgentIdentityStub } from '../../app/agentIdentity/stubs';

function stdioServer(): McpServerConfig {
Expand Down Expand Up @@ -216,6 +216,35 @@ describe('WorkspaceMcpService', () => {
expect(service.sessionHandle().isBaselineServer('late')).toBe(true);
}, 20000);

it('sessionHandle admits servers that finished the initial load before the first baseline read', async () => {
current = { alpha: stdioServer() };
const service = createService();
manager = service.connectionManager();
const handle = service.sessionHandle();

await service.ready;
expect(manager.get('alpha')?.status).toBe('connected');

expect(handle.isBaselineServer('alpha')).toBe(true);
}, 20000);

it('sessionHandle admits a needs-auth server that settled before the first baseline read', async () => {
const server = await startInProcessHttpMcpServer({ authToken: 'secret' });
try {
current = { remote: { transport: 'http', url: server.url } };
const service = createService();
manager = service.connectionManager();
const handle = service.sessionHandle();

await service.ready;
expect(manager.get('remote')?.status).toBe('needs-auth');

expect(handle.isBaselineServer('remote')).toBe(true);
} finally {
await server.close();
}
}, 20000);

it('sessionOverlay marks the ephemeral server names as baseline by construction', async () => {
current = { base: stdioServer() };
const service = createService();
Expand Down
Loading