From 76a311f6e601ea1fe512ef984ec976e1325973d2 Mon Sep 17 00:00:00 2001 From: Search Agent Date: Wed, 15 Jul 2026 15:25:06 +0200 Subject: [PATCH 1/2] fix(protocol-resolver): map CAP 'mcp' protocol to ORD apiProtocol --- __tests__/unit/protocol-resolver.test.js | 43 ++++++++++++++++ __tests__/unit/templates/api-resource.test.js | 51 ++++++++----------- lib/constants.js | 1 + 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/__tests__/unit/protocol-resolver.test.js b/__tests__/unit/protocol-resolver.test.js index d9436b74..011301cb 100644 --- a/__tests__/unit/protocol-resolver.test.js +++ b/__tests__/unit/protocol-resolver.test.js @@ -237,6 +237,49 @@ describe("protocol-resolver", () => { }); }); + describe("with @cap-js/mcp plugin loaded", () => { + beforeEach(() => { + cds.service.protocols["mcp"] = { path: "/mcp", impl: "@cap-js/mcp" }; + }); + + afterEach(() => { + delete cds.service.protocols["mcp"]; + }); + + it("should resolve MCP protocol", () => { + const model = cds.linked(` + @protocol: 'mcp' + service McpService { + entity Books { key ID: UUID; } + } + `); + const srvDefinition = model.definitions["McpService"]; + const result = resolveApiResourceProtocol(srvDefinition); + + expect(result).toHaveLength(1); + expect(result[0].apiProtocol).toBe(ORD_API_PROTOCOL.MCP); + expect(result[0].hasResourceDefinitions).toBe(true); + expect(result[0].entryPoints).not.toContain(null); + expect(loggerWarnSpy).not.toHaveBeenCalled(); + }); + + it("should resolve MCP alongside OData without warning", () => { + const model = cds.linked(` + @protocol: ['odata-v4', 'mcp'] + service MyService { + entity Books { key ID: UUID; } + } + `); + const srvDefinition = model.definitions["MyService"]; + const result = resolveApiResourceProtocol(srvDefinition); + + const protocols = result.map((r) => r.apiProtocol); + expect(protocols).toContain(ORD_API_PROTOCOL.ODATA_V4); + expect(protocols).toContain(ORD_API_PROTOCOL.MCP); + expect(loggerWarnSpy).not.toHaveBeenCalled(); + }); + }); + describe("with custom CAP protocol plugin loaded", () => { beforeEach(() => { cds.service.protocols["custom_protocol"] = { diff --git a/__tests__/unit/templates/api-resource.test.js b/__tests__/unit/templates/api-resource.test.js index 4597fda4..ba08cc84 100644 --- a/__tests__/unit/templates/api-resource.test.js +++ b/__tests__/unit/templates/api-resource.test.js @@ -111,27 +111,18 @@ describe("createAPIResourceTemplate", () => { expect(createAPIResources({ ...appConfig, csn: model })).toEqual([]); }); - it("should create correct resource definition for MCP protocol", () => { - // CAP core doesn't recognize 'mcp' protocol (@protocol: 'mcp' returns empty endpoints) - // Only added when plugin is there - // So we need to mock the protocol resolver to test the MCP resource definition branch - jest.resetModules(); - - jest.doMock("../../../lib/protocol-resolver", () => ({ - resolveApiResourceProtocol: jest.fn().mockReturnValue([ - { - apiProtocol: "mcp", - entryPoints: ["/mcp/mcp-service"], - hasResourceDefinitions: true, - }, - ]), - })); + describe("with @cap-js/mcp plugin loaded", () => { + beforeEach(() => { + cds.service.protocols["mcp"] = { path: "/mcp", impl: "@cap-js/mcp" }; + }); - const { - createAPIResourceTemplate: createAPIResourceTemplateMocked, - } = require("../../../lib/templates/api-resource"); + afterEach(() => { + delete cds.service.protocols["mcp"]; + }); - const model = cds.linked(` + it("should create correct resource definition for MCP protocol", () => { + const model = cds.linked(` + @protocol: 'mcp' service McpService { entity Items { key ID: UUID; @@ -139,20 +130,18 @@ describe("createAPIResourceTemplate", () => { } }; `); - const srvDefinition = model.definitions["McpService"]; - - const apiResourceTemplate = createAPIResourceTemplateMocked(srvDefinition, appConfig); + const srvDefinition = model.definitions["McpService"]; - expect(apiResourceTemplate).toHaveLength(1); - const mcpResource = apiResourceTemplate[0]; - expect(mcpResource.apiProtocol).toBe("mcp"); - expect(mcpResource.resourceDefinitions).toHaveLength(1); - expect(mcpResource.resourceDefinitions[0].type).toBe(MCP_RESOURCE_DEFINITION_TYPE); - expect(mcpResource.resourceDefinitions[0].mediaType).toBe("application/json"); - expect(mcpResource.resourceDefinitions[0].url).toContain(".mcp.json"); + const apiResourceTemplate = createAPIResourceTemplate(srvDefinition, appConfig); - jest.dontMock("../../../lib/protocol-resolver"); - jest.resetModules(); + expect(apiResourceTemplate).toHaveLength(1); + const mcpResource = apiResourceTemplate[0]; + expect(mcpResource.apiProtocol).toBe("mcp"); + expect(mcpResource.resourceDefinitions).toHaveLength(1); + expect(mcpResource.resourceDefinitions[0].type).toBe(MCP_RESOURCE_DEFINITION_TYPE); + expect(mcpResource.resourceDefinitions[0].mediaType).toBe("application/json"); + expect(mcpResource.resourceDefinitions[0].url).toContain(".mcp.json"); + }); }); it('should add apiResources with ORD Extension "visibility=public"', () => { diff --git a/lib/constants.js b/lib/constants.js index ef9c5c92..374fd94a 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -115,6 +115,7 @@ const CAP_TO_ORD_PROTOCOL_MAP = Object.freeze({ "odata-v2": ORD_API_PROTOCOL.ODATA_V2, "rest": ORD_API_PROTOCOL.REST, "graphql": ORD_API_PROTOCOL.GRAPHQL, + "mcp": ORD_API_PROTOCOL.MCP, }); // Protocols that ORD supports but CAP may not recognize (endpoints4 returns []) From 32f431a785f5c777d77ccd65f8354fd9d23e0956 Mon Sep 17 00:00:00 2001 From: Search Agent Date: Wed, 15 Jul 2026 16:42:59 +0200 Subject: [PATCH 2/2] test(protocol-resolver): cover mcp silently skipped when plugin absent --- __tests__/unit/protocol-resolver.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/__tests__/unit/protocol-resolver.test.js b/__tests__/unit/protocol-resolver.test.js index 011301cb..9ddaeb46 100644 --- a/__tests__/unit/protocol-resolver.test.js +++ b/__tests__/unit/protocol-resolver.test.js @@ -280,6 +280,17 @@ describe("protocol-resolver", () => { }); }); + it("should silently skip MCP protocol when plugin is not loaded", () => { + const srvDefinition = { + "name": "McpService", + "@protocol": "mcp", + }; + const result = resolveApiResourceProtocol(srvDefinition); + + expect(result).toEqual([]); + expect(loggerWarnSpy).not.toHaveBeenCalled(); + }); + describe("with custom CAP protocol plugin loaded", () => { beforeEach(() => { cds.service.protocols["custom_protocol"] = {