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
54 changes: 54 additions & 0 deletions __tests__/unit/protocol-resolver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,60 @@ 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();
});
});
Comment thread
zongqichen marked this conversation as resolved.

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"] = {
Expand Down
51 changes: 20 additions & 31 deletions __tests__/unit/templates/api-resource.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,48 +111,37 @@ 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;
name: String;
}
};
`);
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"', () => {
Expand Down
1 change: 1 addition & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 [])
Expand Down