Add MCP capabilities to a CAP service
Learn how easy it is to add an MCP server to your CAP service.
Overview
You will learn
- Understand what the Model Context Protocol is
- How to add MCP server capabilities to your CAP service
- What the high level protocol flow looks like
Prerequisites
Prerequisites
You will need a development environment for CAP Node.js. See the tutorial Set
up a self-contained development environment for CAP
Node.js
The assumptions in this tutorial are based on option 1 or option 2 in that
tutorial, in that you have a VS Code (or GitHub Codespace) environment based on
the foundation repository used in the setup described there, which also means
that your starting directory will be /workspaces/cap-nodejs-dev-env. If you
have your own CAP Node.js development environment setup, then please make the
appropriate adjustments where necessary.
You will also need a coding agent with MCP capabilities. OpenCode is what we
will use for this tutorial. Install it in your self-contained development
environment with npm install --global opencode-ai.
Steps
Intro
Put extremely simply, large Language Models (LLMs) can think (in a next-token-prediction kind of way) but not act, not cause things to happen in the world beyond its own model. There’s a parallel with purely function languages like Haskell, which interacts with the outside world via monads.
In the context of LLMs, the equivalent of monads is the Model Control Protocol (MCP). This is a protocol that provides a unifying layer between LLMs and all sorts of services that can provide information and offer a wide range of facilities.
In CAP, MCP is just another protocol, and through CAP’s “magic made simple” approach, extremely straightforward to configure.
At the time of writing, the MCP adapter in CAP is in beta status.
To have something to which we can add MCP capabilities, let’s set up a very
simple CAP project with a basic “books” oriented service. Conveniently there’s
the tiny-sample facet which we can use.
π Create a new CAP project using this facet, plus the nodejs facet:
cds init --add tiny-sample,nodejs cap-add-mcp-capabilitiesWe’re adding the
nodejsfacet so that we have apackage.jsonfile, as we’ll be installing a package withnpmlater.
π Now open the new cap-add-mcp-capabilities/ directory in a new VS Code /
Codespace window:
code cap-add-mcp-capabilities/This should place you and any new terminal session in the new
cap-add-mcp-capabilities/ directory.
There’s a simple CatalogService with a single Books entity, for which
there are a handful of records. Let’s have a quick look.
π Start the server with cds watch, which should cause log lines as shown
below to be emitted:
[cds] - loaded model from 1 file(s):
srv/cat-service.cds
[cds] - using bindings from: { registry: '~/.cds-services.json' }
[cds] - connect to db > sqlite { database: ':memory:' }
> init from db/data/CatalogService.Books.csv
/> successfully deployed to in-memory database.
[cds] - serving CatalogService {
at: [ '/odata/v4/catalog' ],
decl: 'srv/cat-service.cds:1'
}
[cds] - server listening on { url: 'http://localhost:4004' }The path on which
CatalogServiceis served -/odata/v4/catalog- indicates that this is OData v4.
π Check out the data by visiting http://localhost:4004/odata/v4/catalog/Books, which should show something like this (you may want to use a JSON formatter extension in your browser to see the representation formatted nicely):
{
"@odata.context": "$metadata#Books",
"value": [
{
"ID": 1,
"title": "Wuthering Heights",
"author": "Emily BrontΓ«"
},
{
"ID": 2,
"title": "Jane Eyre",
"author": "Charlotte BrontΓ«"
},
{
"ID": 3,
"title": "The Raven",
"author": "Edgar Allen Poe"
},
{
"ID": 4,
"title": "Eleonora",
"author": "Edgar Allen Poe"
},
{
"ID": 5,
"title": "Catweazle",
"author": "Richard Carpenter"
}
]
}The CDS model is very simple and defined in a single file, srv/cat-service.cds:
@odata
service CatalogService {
entity Books {
key ID : Integer;
title : String;
author : String;
}
}Note the @odata annotation, an explicit declaration that we want this service
to be served with the OData v4 protocol (which is the default behavior anyway).
The separation of layers here, with the service definition being only loosely coupled to the protocol, means that it can be served with different protocols too.
Add an @mcp annotation like this:
@mcp
@odata
service CatalogService {
entity Books {
key ID : Integer;
title : String;
author : String;
}
}π Assuming that the CAP server is still running, notice that in the new log records that are emitted, nothing has changed.
This is because the implementation of MCP capabilities is provided by the MCP
adapter plugin @cap-js/mcp, which isn’t installed.
Let’s address that now.
π First, stop the CAP server (with Ctrl-C), then install the MCP adapter:
npm install @cap-js/mcpπ Now, restart the CAP server, this time asking for more detailed log output for the MCP adapter too:
DEBUG=mcp cds watchExtra information appears, including:
[mcp] - Adapter initialized { service: 'CatalogService' }
[cds] - serving CatalogService {
at: [ '/odata/v4/catalog', '/mcp/catalog' ],
decl: 'srv/cat-service.cds:2'
}
[mcp] - registering MCP services: [ 'CatalogService' ]
[mcp] - Written OpenCode config to: /home/node/.config/opencode/opencode.jsonRemember that MCP servers provide a unified layer to all sorts of services, and speak a simple protocol to enable discovery and usage. Here, we see:
- the MCP server being served at
/mcp/catalog - the
CatalogServicebeing registered with that MCP server
The terms ‘adapter’ and ‘server’ are effectively interchangeable here.
CAP’s developer-focused local-first approach also applies here, where we also see:
- the MCP server is automatically registered to OpenCode, one of two (currently) supported coding agents (the other is Claude Code)
If we were to take a look at the OpenCode configuration at this point, which is
in ~/.config/opencode/opencode.json, we’d see this, which is what was written
by the MCP adapter:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"cds:CatalogService": {
"type": "remote",
"url": "http://localhost:4004/mcp/catalog",
"headers": {
"Authorization": "Basic YWxpY2U6"
},
"enabled": true
}
}
}This is referred to in CAP parlance as “autowiring”.
The Model Context Protocol is based on JSON-RPC, a remote-procedure call style request/response design that is transport-independent; here, HTTP is used (as a transport protocol rather than an application protocol).
π Request the MCP resource at http://localhost:4004/mcp/catalog, whereupon you’ll see something like this:
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Method not allowed."
},
"id": null
}Much of the protocol is done via HTTP POST requests, hence this error, but at least we can see some sort of response, which is all we need to see for now.
Examining the protocol at the JSON-RPC level is best done with a specialized inspector, which is beyond the scope of this tutorial.
π While the CAP server is still running in one shell window, open another shell window and launch OpenCode:
opencodeNote that because of the autowiring, OpenCode knows about the MCP server that you now have running as part of the CAP server - you can see this in the status bar where something similar to this will be displayed:
1 MCP /statusπ Use the /status command to see a bit more information, which should appear
like this:
Status
1 MCP Servers
β’ cds:CatalogService Connectedπ In addition, take a look at the log output from the CAP server, where you should see something like this:
[mcp] - Registered tool { tool: 'describe', service: 'CatalogService' }
[mcp] - Registered generic tool { tool: 'query', service: 'CatalogService' }Based on an initial request by the MCP client built into OpenCode, the MCP
server has replied with information on two key facilities relating to the
CatalogService service: describe and query.
Now that OpenCode is connected to the MCP server that is providing a unified
layer to the CatalogService service with its Books entity, it’s time to try
something out.
π Enter this question:
what books are authored by Edgar Allen Poe?π Observe what is logged in the MCP component of the CAP server:
[mcp] - describe { service: 'CatalogService', entities: [ 'Books' ] }
...
[mcp] - query {
service: 'CatalogService',
cql: "SELECT title from Books where author = 'Edgar Allen Poe'"
}What has happened is that the MCP client component of OpenCode has:
describe: requested a detailed description of the service and entityquery: sent a query written in CQL to the MCP server, which has facilitated a compilation and execution of that query with the CAP service and returned the result
This flow is shown in OpenCode, as well as the response:
+ Thought: 564ms
The user is asking about books authored by Edgar Allen Poe.
Let me use the CatalogService to query for this.
First, let me describe the data model.
* cds_CatalogService_describe
* cds_CatalogService_query
[cql=SELECT title from Books where author = 'Edgar Allen Poe']
- The Raven
- EleonoraHere’s a screenshot of what it might look like:

You’ve just wired up your first combination of CAP service, MCP adapter (server) and MCP client, and put it to use. Well done!
- The Capire topic Model Context Protocol Adapter is definitely worth digging into
- An overview of the Monad on the Haskell Wiki
- The cds add section of Capire has an overview of the facets available
- Learn about how the CDS plugin mechanism works in the three part series CAP Nodejs plugins
- More on JSON-RPC
- There’s a section on Autowired MCP Clients in Capire
To answer the question, you may wish to refer to how Basic Authentication
details are
encoded, and to
keep in mind the pre-defined mock
users that exist
for the default authentication strategy (which is mocked) when running
non-productively.
Resources
Discussion
Share feedback on this tutorial or join the conversation in SAP Community.