Status: GA on DEV. Curated toolset shipped with #912. Adapter: @cap-js/mcp@1.1.1.
Upstream reference: CAP MCP protocol adapter.
See also:
- End-user quickstart: docs/end-users/mcp-quickstart.md
- Operator runbook: docs/developers/operations/mcp-server.md
MCP (Model Context Protocol) is a JSON-RPC 2.0 protocol for exposing tools, resources, and prompts to LLM clients (Claude Desktop, Claude Code, Cursor, custom agents). The CAP adapter @cap-js/mcp mounts a JSON-RPC endpoint per service and auto-generates:
describe— returns the service's CSN (entities, actions, functions, types). No arguments.query— a generic CQN-SELECTtool. Arguments:entity,select?,where?,top?,skip?.- Per-action/function tools — one MCP tool per CDS action or function, because this project sets
cds.mcp.per_action_tool: true. Each tool inherits the CDS signature verbatim.
The auto-generated tool shapes (describe, query) are documented at https://cap.cloud.sap/docs/guides/protocols/mcp — do not re-document them here.
MCP is mounted at /mcp/<service-@path>. Three services expose MCP in this project:
| Service | MCP endpoint | Auth |
|---|---|---|
SearchService |
/mcp/search |
Public |
HomepageService |
/mcp/homepage |
Public (per-function overrides may apply) |
KnowledgeGraphService |
/mcp/graph |
Public (admin actions carry their own @requires) |
Local: http://localhost:4004/mcp/search.
DEV: https://tutorials-approuter-dev.cfapps.eu10-005.hana.ondemand.com/mcp/search.
Every enabled service also serves OData at the same base path (/search, /homepage, /graph). MCP is additive — the OData mount is untouched. This is why the service annotations look like @protocol: ['odata', 'graphql', 'mcp'] and not @mcp alone. The @mcp single-protocol shortcut REPLACES the default OData mount; see [[cap-graphql-shortcut-replaces-odata]] in MEMORY.md and the callout comments in srv/search-service.cds:18-22.
Every tool invocation is a JSON-RPC 2.0 request with method tools/call:
{
"jsonrpc": "2.0",
"id": "<any string or number>",
"method": "tools/call",
"params": {
"name": "<tool name>",
"arguments": { "<arg>": "<value>" }
}
}Success response:
{
"jsonrpc": "2.0",
"id": "<echo of request id>",
"result": {
"content": [ { "type": "text", "text": "<JSON-stringified return value>" } ]
}
}Error response (JSON-RPC error object):
{
"jsonrpc": "2.0",
"id": "<echo>",
"error": {
"code": -32602,
"message": "Invalid params",
"data": { "details": "..." }
}
}Standard JSON-RPC codes: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error. CAP handler errors surface via req.error() and land in error.data (for example { code: 'KG_LOOKUP_FAILED', message: '...' } from kg_prerequisites).
Ten tools are curated in the CDS surfaces (five in SearchService, three in HomepageService, two in KnowledgeGraphService). Each also appears as an OData function at the same URL — the MCP wrapper reuses the CDS handler verbatim.
Purpose. Fuzzy full-text search across published tutorials — returns slug + title + short snippet + tag list.
Endpoint. /mcp/search
| Argument | Type | Required | Notes |
|---|---|---|---|
query |
String |
no | Natural-language search terms; word-boundary matched, stopword-filtered. |
tags |
array<String> |
no | Exact-match filter on primaryTag. |
experience |
String |
no | One of 'beginner', 'intermediate', 'advanced'. |
limit |
Integer |
no | Default 10, hard max 100. |
Return shape (from handler, srv/search-service.js:273):
Example.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_tutorials",
"arguments": { "query": "CAP HANA", "experience": "beginner", "limit": 5 }
}
}Purpose. List published missions with the number of tutorials in each — the same missions the /missions/ page shows.
Endpoint. /mcp/search
| Argument | Type | Required | Notes |
|---|---|---|---|
tags |
array<String> |
no | Returns only missions whose primaryTag matches any supplied value. |
limit |
Integer |
no | Default 20, hard max 50. |
Return shape (from handler, srv/search-service.js:327):
[
{
"slug": "string (lowercased)",
"title": "string",
"description": "string",
"tutorialCount": 0
}
]Example.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "list_missions",
"arguments": { "limit": 20 }
}
}Purpose. Fetch a mission by slug with its ordered tutorial list. Returns null for unknown/unpublished missions; slug is case-insensitive.
Endpoint. /mcp/search
| Argument | Type | Required | Notes |
|---|---|---|---|
slug |
String |
yes | Mission slug (lowercased server-side). |
Return shape (from handler, srv/search-service.js:395):
{
"slug": "string",
"title": "string",
"description": "string",
"tutorials": [
{ "slug": "string", "title": "string", "order": 0 }
]
}Returns null when no published mission matches the slug.
Example.
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_mission",
"arguments": { "slug": "cap-fiori-app" }
}
}Purpose. Fetch tutorial metadata and ordered step list by slug. Returns null for unknown slugs, empty slugs, or INACTIVE tutorials.
Endpoint. /mcp/search
| Argument | Type | Required | Notes |
|---|---|---|---|
slug |
String |
yes | Tutorial slug (case-insensitive; lowercased server-side). |
Return shape (from handler, srv/search-service.js:428):
{
"slug": "string",
"title": "string",
"description": "string",
"tags": ["string"], // [primaryTag] or [] when null
"steps": [
{ "number": 0, "title": "string" }
]
}Note the handler maps the DB column stepOrder onto the returned number field, matching the CDS return type.
Example.
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "get_tutorial",
"arguments": { "slug": "hana-cloud-mission-1-onboarding" }
}
}Purpose. Recent SAP developer news items — the same feed the homepage news band shows.
Endpoint. /mcp/homepage
| Argument | Type | Required | Notes |
|---|---|---|---|
limit |
Integer |
no | Default 10, hard max 50. |
Return shape. array of RssItem (declared in srv/homepage-service.cds:75, populated by fetchRssItems in the handler at srv/homepage-service.js:829):
[
{
"title": "string",
"link": "string",
"publishedAt": "2026-07-08T09:44:00.000Z",
"description": "string"
}
]The MCP tool bypasses the homepage news band's hardcoded limit:2 and calls fetchRssItems(SAP_NEWS_RSS_URL, { limit }) directly, so callers can request up to 50.
Example.
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "get_recent_news",
"arguments": { "limit": 10 }
}
}Purpose. Recent SAP developer videos from the persistent Videos corpus, ordered by publish date descending. Corpus is refreshed twice-weekly by srv/jobs/fetch-videos-job.js.
Endpoint. /mcp/homepage
| Argument | Type | Required | Notes |
|---|---|---|---|
limit |
Integer |
no | Default 10, hard max 50. |
Return shape. array of VideoItem (declared in srv/homepage-service.cds:73; handler at srv/homepage-service.js:850):
[
{
"videoId": "string (YouTube video id)",
"title": "string",
"thumbnail": "string (URL)",
"publishedAt": "2026-07-08T09:44:00.000Z"
}
]Returns [] on any DB failure — callers never see a 500.
Example.
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "get_recent_videos",
"arguments": { "limit": 10 }
}
}Purpose. Tutorials that teach concepts this tutorial depends on. Answers "what should I learn first?". Backed by the same knowledge graph the tutorial sidebar uses.
Endpoint. /mcp/graph
| Argument | Type | Required | Notes |
|---|---|---|---|
tutorial_slug |
String |
yes | Tutorial slug (lowercased server-side). |
depth |
Integer |
no | Default 10, hard max 50. Slices the prerequisitesOf arm. |
Return shape. array of TutorialRef (declared in srv/knowledge-graph-service.cds:110):
[
{
"slug": "string",
"title": "string",
"weight": 0.00, // Decimal(3,2), 0.00–1.00
"reason": "string"
}
]Handler at srv/knowledge-graph-service.js:1357 re-uses the internal neighborhood() handler and returns nb.prerequisitesOf.slice(0, depth). On lookup failure the handler emits a JSON-RPC error with code: 'KG_LOOKUP_FAILED' and returns [].
Example.
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "kg_prerequisites",
"arguments": { "tutorial_slug": "hana-cloud-mission-3-modelling", "depth": 5 }
}
}Purpose. Tutorials that build on what this one teaches. Answers "what should I learn next?". PageRank-blended when KG_PAGERANK_ENABLED=true (#916).
Endpoint. /mcp/graph
| Argument | Type | Required | Notes |
|---|---|---|---|
tutorial_slug |
String |
yes | Tutorial slug (lowercased server-side). |
limit |
Integer |
no | Default 10, hard max 50. Slices the whatToLearnNext arm. |
Return shape. array of TutorialRef — same shape as kg_prerequisites above.
Handler at srv/knowledge-graph-service.js:1376 also re-uses neighborhood() and slices the whatToLearnNext arm. Same error posture as kg_prerequisites.
Example.
{
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "kg_what_to_learn_next",
"arguments": { "tutorial_slug": "hana-cloud-mission-3-modelling", "limit": 10 }
}
}Two additional anonymous tools extend the public MCP surface with the community-events catalog and full news-article bodies. Both are @requires: 'any'.
Purpose. Search the public SAP community events catalog — CodeJams, Devtoberfest, TechEd, and user-group events. The same events shown on the homepage events band, but fully searchable and filterable. Ordered by start date (soonest first).
Endpoint. /mcp/search
| Argument | Type | Required | Notes |
|---|---|---|---|
query |
String |
no | Case-insensitive substring match on event title and description. |
eventType |
String |
no | One of 'codejam', 'teched', 'devtoberfest', 'usergroup'. Unknown values are ignored (no filter). |
region |
String |
no | 'AMERICAS', 'EMEA', 'APJ', 'VIRTUAL', or 'ALL' (default). 'VIRTUAL' matches virtualOrInPerson='virtual'. |
upcomingOnly |
Boolean |
no | Default true — only events not yet ended (in-progress multi-day events are kept). false includes past events. |
limit |
Integer |
no | Default 20, hard max 50. |
Return shape (handler srv/lib/mcp-events-search.js, backed by CommunityEvents in db/external-content.cds):
[
{
"slug": "string",
"title": "string",
"eventType": "string",
"description": "string",
"location": "string",
"region": "AMERICAS | EMEA | APJ | UNKNOWN",
"isVirtual": false,
"startDate": "2026-10-01",
"endDate": "2026-10-02",
"url": "string"
}
]Fails open — returns [] on any DB error, never a 500.
Example.
{
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {
"name": "search_events",
"arguments": { "query": "CAP", "eventType": "codejam", "region": "EMEA", "limit": 10 }
}
}Purpose. Fetch the full article body of one SAP Developer News item by URL. Complements get_recent_news (which returns only title/link/summary): pass a news item's link and this server-fetches the article, strips it to readable text, and returns it with metadata.
Endpoint. /mcp/homepage
| Argument | Type | Required | Notes |
|---|---|---|---|
url |
String |
yes | The article link from get_recent_news. Must be an SAP news host (news.sap.com, community.sap.com, blogs.sap.com) or subdomain — other hosts are rejected with a JSON-RPC error (SSRF guard). |
Return shape (handler srv/lib/mcp-news-detail.js):
{
"title": "string",
"url": "string",
"publishedAt": "2026-08-01T09:00:00Z", // from article:published_time meta, may be null
"summary": "string", // og:description / meta description
"content": "string (readable body text, capped at 20k chars)",
"fetchedAt": "2026-08-22T10:00:00.000Z"
}Server-fetches with an 8s timeout and a 1-hour read-through cache keyed by URL. A disallowed host returns a 400-class error; an upstream failure returns 502.
Example.
{
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "get_news_detail",
"arguments": { "url": "https://news.sap.com/2026/08/some-episode/" }
}
}Every MCP-enabled service also exposes:
describe— no arguments; returns the CSN slice for this service (entities + actions + functions + types).query— argumentsentity(required),select?,where?,top?,skip?; runs a CQNSELECTagainst a queryable entity in scope. Shapes are documented at https://cap.cloud.sap/docs/guides/protocols/mcp.
Because cds.mcp.per_action_tool: true is set on this project, every action and function in each MCP-enabled service is also a first-class tool — not just the eight curated names listed above. For example, SearchService.getFacets is reachable as an MCP tool at /mcp/search, and KnowledgeGraphService.neighborhood at /mcp/graph. Curated tools are the ones with LLM-friendly signatures, snake_case names, and doc-comments intended for tool descriptions; the rest are available but were not shaped for MCP-first consumption. Admin actions on KnowledgeGraphService (runSparql, mergeConcepts, vetoConcept, triggerGraphRebuild, publishAllConcepts) remain gated by @requires: 'KnowledgeGraph.Admin' when invoked via MCP.
Start the CAP backend:
cds watchThen hit the MCP endpoint with tools/call. Example — search_tutorials:
curl -X POST "http://localhost:4004/mcp/search" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_tutorials",
"arguments": { "query": "CAP HANA", "limit": 3 }
}
}'Example — describe (introspect the service):
curl -X POST "http://localhost:4004/mcp/search" \
-H "Content-Type: application/json" \
-d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "describe", "arguments": {} } }'Example — query over SearchableItems:
curl -X POST "http://localhost:4004/mcp/search" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "query",
"arguments": { "entity": "SearchService.SearchableItems", "top": 3 }
}
}'Success responses come back wrapped in the standard result.content[0].text envelope with the handler's return value JSON-stringified. Failure responses use the standard JSON-RPC error object.
Phase 2 adds nine authenticated tools across two services: seven on DeveloperService (user progress + step content) and two on HomepageService (personalized recommendations). All nine require a valid XSUAA JWT or a PAT with at least read scope.
| Client type | Route | Auth mechanism |
|---|---|---|
| Browser agent (Claude Desktop OAuth) | /mcp-auth/api |
OAuth 2.1 + PKCE via XSUAA/IAS |
| Headless agent (Claude Code, CI, VS Code extension) | /mcp-pat/api |
Bearer PAT (pat_...) |
| Anonymous / public content | /mcp/* |
none |
/mcp-auth/* carries the full XSUAA bearer from the approuter. /mcp-pat/* is handled by srv/lib/mcp-pat-middleware.js before the CAP runtime sees the request — the middleware resolves the PAT to a synthetic req.user and attaches tokenSource: 'pat'. Both paths converge on resolveDbUser() and the same CAP handlers.
Purpose. The authenticated user's tutorials filtered by progress status.
Endpoint. /mcp-auth/api or /mcp-pat/api Auth. @requires: 'authenticated-user'
| Argument | Type | Required | Notes |
|---|---|---|---|
status |
String |
no | 'in_progress', 'completed', 'all' (default). |
limit |
Integer |
no | Default 20, max 50. |
Return shape. array of TutorialProgress — slug, title, progress fields. See srv/developer-service.cds.
Purpose. The authenticated user's missions filtered by status.
Endpoint. /mcp-auth/api or /mcp-pat/api Auth. @requires: 'authenticated-user'
| Argument | Type | Required | Notes |
|---|---|---|---|
status |
String |
no | 'in_progress', 'completed', 'not_started', 'all' (default). |
limit |
Integer |
no | Default 10, max 50. |
Purpose. The authenticated user's registered events.
Endpoint. /mcp-auth/api or /mcp-pat/api Auth. @requires: 'authenticated-user'
| Argument | Type | Required | Notes |
|---|---|---|---|
when |
String |
no | 'upcoming' (default), 'past', 'registered'. |
limit |
Integer |
no | Default 20, max 50. |
Purpose. Step numbers the user has completed for a specific tutorial.
Endpoint. /mcp-auth/api or /mcp-pat/api Auth. @requires: 'authenticated-user'
| Argument | Type | Required | Notes |
|---|---|---|---|
slug |
String |
yes | Tutorial slug (lowercased server-side). |
Return shape. array of Integer — the completed step numbers. Returns 404 for unknown slugs.
Purpose. Full HTML slice of a tutorial step. Available on both the anonymous (/mcp/search) and authenticated (/mcp-auth/api) routes — authenticated callers also emit a tokenSource-tagged metric.
Endpoint. /mcp/search, /mcp-auth/api, or /mcp-pat/api Auth. None required (anonymous mount), or @requires: 'authenticated-user' (authenticated mount).
| Argument | Type | Required | Notes |
|---|---|---|---|
slug |
String |
yes | Tutorial slug. |
stepNumber |
Integer |
yes | 1-based step number. |
Return shape.
{
"slug": "string",
"stepNumber": 1,
"stepTitle": "string",
"html": "string (HTML fragment, gzip-decoded from HANA BLOB)",
"textLength": 0,
"totalSteps": 0
}Returns 404 if the tutorial or step is not in the content store. Backed by srv/lib/tutorial-step-slicer.js; cached via the shared caching service (cds-caching, #1180) keyed by slice:<slug>::<activeManifestVersion>. Disabled if KG_STEP_SLICER_ENABLED=false.
Purpose. Mark a tutorial step as completed. Writes progress; requires pat-write pseudo-role for PAT callers.
Endpoint. /mcp-auth/api or /mcp-pat/api Auth. @requires: 'authenticated-user'. PAT callers need scopes: ['read', 'write'].
| Argument | Type | Required | Notes |
|---|---|---|---|
slug |
String |
yes | Tutorial slug. |
stepNumber |
Integer |
yes | 1-based step number. |
Delegates to the existing completeStep action — the same audit trail fires for browser and MCP callers.
Purpose. Reset all step progress for a tutorial. Writes progress; requires pat-write pseudo-role for PAT callers. Emits TutorialProgressReset audit event with tokenSource field.
Endpoint. /mcp-auth/api or /mcp-pat/api Auth. @requires: 'authenticated-user'. PAT callers need scopes: ['read', 'write'].
| Argument | Type | Required | Notes |
|---|---|---|---|
slug |
String |
yes | Tutorial slug. |
Purpose. Persona-ranked tutorial recommendations from HomepageForYouCandidates.
Endpoint. /mcp-auth/homepage or /mcp-pat/homepage Auth. @requires: 'authenticated-user'
| Argument | Type | Required | Notes |
|---|---|---|---|
limit |
Integer |
no | Default 10, max 50. |
Return shape. array of TutorialRef (slug, title, snippet, tags) — ranked by persona fit. Anonymous users (no UserLearningPreferences) receive the un-personalized pool.
Purpose. Persona-ranked mission recommendations from HomepageForYouCandidates.
Endpoint. /mcp-auth/homepage or /mcp-pat/homepage Auth. @requires: 'authenticated-user'
| Argument | Type | Required | Notes |
|---|---|---|---|
limit |
Integer |
no | Default 10, max 50. |
Return shape. array of MissionRef (slug, title, description, tutorialCount).
Four additional tools on KnowledgeGraphService, reachable at /mcp/graph. All anonymous; no auth required.
| Tool | Args | Returns |
|---|---|---|
kg_shared_concepts |
slug_a, slug_b |
concept overlap [{conceptSlug, name}] |
kg_neighborhood |
slug, depth? |
four arms {prerequisites, whatToLearnNext, sharedConcepts, teaches} |
kg_search_concepts |
query, maxConcepts?, maxTutorials? |
{concepts[], tutorials[]} |
kg_community |
id (community fingerprint) |
{communityId, label, memberTutorials[], size, promotedToMissionSlug} |
Notes:
kg_shared_conceptsreturns{conceptSlug, name}pairs — there is noscorefield (always-zero in the original algorithm, removed for clarity).kg_communitytakes the community fingerprint (a stableString(64)key), not a numeric ID. Read-only; DEV-only until #917 promotion reaches PROD.
These tools are mounted under /mcp-admin/* by srv/lib/mcp-compose-router.js. They require a valid XSUAA bearer with the scopes listed below — the approuter enforces XSUAA on /mcp-admin/*.
All admin tools also require the service-level
Adminscope (CAP ANDs it with the per-tool scope shown). A caller withTutorial.MCP+Tutorial.Authorbut withoutAdminwill receive a 403 from the service layer.
| Tool | Required scope | Wraps |
|---|---|---|
merge_concepts |
KnowledgeGraph.Admin |
KnowledgeGraphService.mergeConcepts action |
promote_community_to_mission |
SuperAdmin |
AdminService.promoteCommunityToMission action |
trigger_rebuild |
Tutorial.Author |
GitHub rebuild-content.yml workflow dispatch (preferred path) |
publish_content |
SuperAdmin |
In-process POST /content/publish (emergency lever; requires CONTENT_API_KEY configured — returns 503 otherwise) |
publish_contentrequires SuperAdmin (notTutorial.Author) because it is an emergency lever that bypasses the normal CI validation path. Prefertrigger_rebuildfor routine content updates.
Three resource URI schemes are served by the compose router. The compose router is mounted at /mcp/graph (KnowledgeGraphService) and /mcp/admin (AdminService). These mounts are also reachable on the authenticated surfaces: the approuter rewrites /mcp-auth/* → srv /mcp/* and /mcp-pat/* → srv /mcp/*, so resources are accessible at all three surfaces. There is no /mcp-auth/api resources endpoint.
| URI scheme | What it returns |
|---|---|
tutorial://<slug> |
Tutorial metadata, step titles, and rendered HTML |
mission://<slug> |
Mission and its ordered tutorial list |
concept://<id> |
Knowledge-graph concept and the tutorials that teach it |
Discover available resources with resources/list; fetch one with resources/read <uri>.
Four reusable prompt templates, discoverable via prompts/list on any Phase-3-enabled endpoint:
| Prompt | Arguments | What it does |
|---|---|---|
summarize_mission_for_beginner |
mission_slug |
Beginner-friendly mission summary |
generate_lab_exercise |
tutorial_slug, step? |
Hands-on lab from a tutorial step |
explain_concept |
concept_id |
Explains a KG concept and its tutorials |
suggest_learning_path |
from_slug, to_slug |
Ordered path between two tutorials |
Invoke with prompts/get; the client fills in the arguments.
- End-user quickstart (Claude Desktop / Claude Code wiring): docs/end-users/mcp-quickstart.md
- Operator runbook (enable/disable, kill switch, smoke tests): docs/developers/operations/mcp-server.md
- HCQL protocol adapter (sibling protocol, same URL pattern): hcql-support.md
- Issue: sap-tutorials/tutorials-ims#912
- Upstream: CAP MCP protocol adapter
[ { "slug": "string (lowercased)", "title": "string", "snippet": "string (first 240 chars of description)", "tags": ["string"] // [primaryTag] or [] when null } ]