Skip to content

Latest commit

 

History

History
792 lines (570 loc) · 26.7 KB

File metadata and controls

792 lines (570 loc) · 26.7 KB

MCP Server (Model Context Protocol)

Status: GA on DEV. Curated toolset shipped with #912. Adapter: @cap-js/mcp@1.1.1.

Upstream reference: CAP MCP protocol adapter.

See also:


What is MCP?

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:

  1. describe — returns the service's CSN (entities, actions, functions, types). No arguments.
  2. query — a generic CQN-SELECT tool. Arguments: entity, select?, where?, top?, skip?.
  3. 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.

Base URLs

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.

JSON-RPC envelope

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).


Curated tools

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.

1. search_tutorials

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):

[
  {
    "slug":    "string (lowercased)",
    "title":   "string",
    "snippet": "string (first 240 chars of description)",
    "tags":    ["string"]            // [primaryTag] or [] when null
  }
]

Example.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_tutorials",
    "arguments": { "query": "CAP HANA", "experience": "beginner", "limit": 5 }
  }
}

2. list_missions

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 }
  }
}

3. get_mission

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" }
  }
}

4. get_tutorial

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" }
  }
}

5. get_recent_news

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 }
  }
}

6. get_recent_videos

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 }
  }
}

7. kg_prerequisites

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 }
  }
}

8. kg_what_to_learn_next

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 }
  }
}

Tier 2 curated tools

Two additional anonymous tools extend the public MCP surface with the community-events catalog and full news-article bodies. Both are @requires: 'any'.

search_events

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 }
  }
}

get_news_detail

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/" }
  }
}

Auto tools (all services)

Every MCP-enabled service also exposes:

  • describe — no arguments; returns the CSN slice for this service (entities + actions + functions + types).
  • query — arguments entity (required), select?, where?, top?, skip?; runs a CQN SELECT against 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.


Testing this locally

Start the CAP backend:

cds watch

Then 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: Authenticated tools

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.

Route decision matrix

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.

9. get_my_tutorials

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.


10. get_my_missions

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.

11. get_my_events

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.

12. get_my_completed_steps

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.


13. get_tutorial_step

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.


14. complete_step

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.


15. reset_tutorial_progress

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.

16. get_my_recommended_tutorials

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.


17. get_my_recommended_missions

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).



Phase 3 KG deep-dive tools (anonymous, /mcp/graph)

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_concepts returns {conceptSlug, name} pairs — there is no score field (always-zero in the original algorithm, removed for clarity).
  • kg_community takes the community fingerprint (a stable String(64) key), not a numeric ID. Read-only; DEV-only until #917 promotion reaches PROD.

Phase 3 admin tools (/mcp-admin/*, XSUAA-gated)

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 Admin scope (CAP ANDs it with the per-tool scope shown). A caller with Tutorial.MCP + Tutorial.Author but without Admin will 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_content requires SuperAdmin (not Tutorial.Author) because it is an emergency lever that bypasses the normal CI validation path. Prefer trigger_rebuild for routine content updates.


Phase 3 resources

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>.


Phase 3 prompts

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.


Related