fix(mcp): map OAuth flow failures to wire code 40929

This commit is contained in:
7Sageer 2026-08-20 16:32:20 +08:00
parent 3cf106152b
commit 2e9ed91390
5 changed files with 25 additions and 5 deletions

View file

@ -62,6 +62,7 @@ export const ErrorCode = {
RUNTIME_UNAVAILABLE: 40926,
PROMPT_ID_CONFLICT: 40927,
MCP_MANAGEMENT_DISABLED: 40928,
MCP_OAUTH_FAILED: 40929,
APPROVAL_EXPIRED: 41001,
QUESTION_EXPIRED: 41002,

View file

@ -199,6 +199,9 @@ function sendMappedError(
errEnvelope(ErrorCode.MCP_MANAGEMENT_DISABLED, err.message, requestId, err.stack),
);
return;
case ErrorCodes.MCP_OAUTH_FAILED:
reply.send(errEnvelope(ErrorCode.MCP_OAUTH_FAILED, err.message, requestId, err.stack));
return;
}
}
throw err;

View file

@ -358,6 +358,22 @@ describe('server /api/v2/mcp', () => {
expect(res.body.data).toBeNull();
});
it('maps the engine mcp.oauth_failed rejection to 40929', async () => {
const stub = makeMcpStub();
stub.service.completeServerAuth = async () => {
throw new Error2(
ErrorCodes.MCP_OAUTH_FAILED,
'OAuth flow for "a" failed: OAuth callback timed out',
);
};
await boot(stub);
const res = await call('POST', '/api/v2/mcp/auth:complete', { flowId: 'flow-1' });
expect(res.status).toBe(200);
expect(res.body.code).toBe(40929);
expect(res.body.data).toBeNull();
});
it('maps a delete rejected with mcp.server_not_found to 40408', async () => {
const stub = makeMcpStub();
stub.service.removeServer = async (name) => {

View file

@ -68,8 +68,8 @@ const NOT_FOUND = 40404;
/** kap-server wire codes mirrored so memory/ipc surface the same numeric codes as `/api/v2/mcp`. */
const MCP_SERVER_NOT_FOUND = 40408;
const MCP_MANAGEMENT_DISABLED = 40928;
const MCP_OAUTH_FAILED = 40929;
const PROMPT_ID_CONFLICT = 40927;
const INTERNAL_ERROR = 50001;
/** Wire name of the engine's `IMcpManagementService` decorator id. */
const MCP_MANAGEMENT_SERVICE = 'mcpManagementService';
@ -92,7 +92,7 @@ function rethrowFileErrorAsRpc(error: unknown): never {
* `RPCError`s carrying the kap-server wire codes, so memory and ipc behave
* identically (a raw `Error2` would cross ipc as a generic 50001) and both
* match `/api/v2/mcp` `mcp.server_not_found` 40408, `request.invalid` /
* `config.invalid` 40001.
* `config.invalid` 40001, `mcp.oauth_failed` 40929.
*/
function rethrowMcpManagementErrorAsRpc(error: unknown): never {
if (error instanceof Error2) {
@ -103,7 +103,7 @@ function rethrowMcpManagementErrorAsRpc(error: unknown): never {
case ErrorCodes.CONFIG_INVALID:
throw new RPCError(REQUEST_INVALID, error.message, error.details);
case ErrorCodes.MCP_OAUTH_FAILED:
throw new RPCError(INTERNAL_ERROR, error.message, error.details);
throw new RPCError(MCP_OAUTH_FAILED, error.message, error.details);
}
}
throw error;

View file

@ -473,7 +473,7 @@ export function defineKlientConformance(
}
});
it('global mcp OAuth failures use transport-stable 50001 errors', async () => {
it('global mcp OAuth failures map to the 40929 wire code on every transport', async () => {
const mcp = target.klient.global.mcp;
const flags = target.app.accessor.get(IFlagService);
flags.setConfigOverrides({ mcp_management: true });
@ -489,7 +489,7 @@ export function defineKlientConformance(
try {
await expect(
mcp.beginAuth({ locator: { source: 'global', name: 'conf-oauth-failure' } }),
).rejects.toMatchObject({ name: 'RPCError', code: 50001 });
).rejects.toMatchObject({ name: 'RPCError', code: 40929 });
} finally {
await mcp.remove({ name: 'conf-oauth-failure' });
}