Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6f0462e
ci(rebuild-qa): add force-publish input to re-seed Option B ContentCu…
jung-thomas Aug 27, 2026
c9fa587
feat(content): move CONTENT_DELTA_* flags from env to ImsConfig DB co…
jung-thomas Aug 27, 2026
67f742f
Merge pull request #2061 from sap-tutorials/feat/content-delta-flags-…
jung-thomas Aug 28, 2026
4b3d08d
Merge pull request #2062 from sap-tutorials/fix/qa-force-publish-input
jung-thomas Aug 28, 2026
250d28f
feat(content): default Option-B delta flags ON — seed ImsConfig 'true…
jung-thomas Aug 28, 2026
5744947
Merge pull request #2064 from sap-tutorials/feat/content-delta-defaul…
jung-thomas Aug 28, 2026
f2f1ef4
feat(feature-flags): migrate 14 env feature flags to DB config (#2060)
jung-thomas Aug 28, 2026
c203773
feat(feature-flags): make the Feature Flags admin app editable (#2060)
jung-thomas Aug 28, 2026
2481fdb
Merge pull request #2066 from sap-tutorials/feat/env-flags-to-db-config
jung-thomas Aug 28, 2026
55bccff
fix(content): skip Option-B delta flag boot-seed under vitest
jung-thomas Aug 28, 2026
21a6186
Merge pull request #2068 from sap-tutorials/fix/content-delta-boot-se…
jung-thomas Aug 28, 2026
7739f17
fix(qa): slug-scope QA content publish + resilient commit + lock release
jung-thomas Aug 28, 2026
8108580
test: seed content.delta.* flags OFF in ImsConfig test data so ensure…
jung-thomas Aug 28, 2026
2bec9d9
Merge pull request #2069 from sap-tutorials/fix/qa-slug-scoped-publish
jung-thomas Aug 28, 2026
6194a5e
Merge pull request #2070 from sap-tutorials/fix/env-flags-test-fallout
jung-thomas Aug 28, 2026
9ff00be
fix(tokens): sign-in gate must check /auth/user JSON, not resp.ok
jung-thomas Aug 28, 2026
024baa4
Merge pull request #2071 from sap-tutorials/fix/tokens-auth-gate-1093
jung-thomas Aug 28, 2026
9e51320
fix(me): AllCompletions/RecentActivity sign-in gate must check /auth/…
jung-thomas Aug 28, 2026
7e239e5
Merge pull request #2072 from sap-tutorials/fix/me-islands-auth-gate
jung-thomas Aug 28, 2026
2a10624
fix(feature-flags): don't connect to db before model is loaded (fixes…
jung-thomas Aug 28, 2026
201ba78
Merge pull request #2073 from sap-tutorials/fix/env-flags-attachments…
jung-thomas Aug 28, 2026
62dbde4
docs: design spec for .well-known MCP OAuth discovery surface
jung-thomas Aug 28, 2026
a579a17
fix(freshness): calibrate prompt by severity so findings resurface
jung-thomas Aug 28, 2026
000b1fd
Merge pull request #2074 from sap-tutorials/worktree-freshness-prompt…
jung-thomas Aug 28, 2026
6a447d9
docs: revise .well-known spec to reality + add implementation plan
jung-thomas Aug 28, 2026
521a8db
feat(approuter): serve /.well-known/openid-configuration as oauth-aut…
jung-thomas Aug 28, 2026
a0b3a2d
fix: restore original well-known-oauth tests and append openid-config…
jung-thomas Aug 28, 2026
5985535
feat(approuter): serve /.well-known/mcp.json courtesy manifest
jung-thomas Aug 28, 2026
79f86ea
feat(approuter): add resource_metadata WWW-Authenticate pointer on /m…
jung-thomas Aug 28, 2026
50ddd8e
test(hybrid): assert openid-configuration + mcp.json on deployed appr…
jung-thomas Aug 28, 2026
15b047b
docs: correct .well-known architecture + add Akamai edge-forward request
jung-thomas Aug 28, 2026
99a53b2
docs: clarify openid-configuration alias body + Akamai verify scope
jung-thomas Aug 28, 2026
e5752cf
Merge pull request #2075 from sap-tutorials/worktree-well-known-disco…
jung-thomas Aug 28, 2026
7a476c9
docs: register akamai-well-known-forward in sidebar (fixes docs:build)
jung-thomas Aug 28, 2026
338190f
Merge pull request #2076 from sap-tutorials/fix/docs-sidebar-akamai
jung-thomas Aug 28, 2026
33cff32
chore(release): bump MTA 1.20.0 → 1.20.1 + refresh What's New digest
jung-thomas Aug 28, 2026
9933e4f
fix(homepage): boot-seed HomepageShelves; retire seed CSV + manual se…
jung-thomas Aug 28, 2026
05d76e1
Merge pull request #2079 from sap-tutorials/fix/homepage-shelves-dura…
jung-thomas Aug 28, 2026
96d1725
Merge pull request #2078 from sap-tutorials/chore/release-1.20.1
jung-thomas Aug 28, 2026
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
21 changes: 11 additions & 10 deletions .deploy/mta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ ID: tutorials-ims
# Bump this on each release you deploy — it's the version shown by `cf mtas`
# and in the mtar filename (tutorials-ims_<version>.mtar). Deploy is manual:
# `cd .deploy && mbt build && cf deploy mta_archives/tutorials-ims_<version>.mtar -e ../deploy/<env>.mtaext -f`.
version: 1.20.0
version: 1.20.1

# Top-level parameters (overridable per-env via deploy/<env>.mtaext).
parameters:
Expand Down Expand Up @@ -117,14 +117,15 @@ modules:
KNOWLEDGE_GRAPH_ENABLED: 'false'
KG_EXTRACT_BUILD_CAP: '200'
KG_MERGE_SIM_THRESHOLD: '0.92'
# Phase 2 MCP feature flags (Task 15 #1105). Default 'true'. Override via cf set-env.
MCP_AUTH_ENABLED: 'true'
MCP_PAT_MINT_ENABLED: 'true'
KG_STEP_SLICER_ENABLED: 'true'
# KG_PATH_V2 (issue #913): property-graph shortest-path engine backing
# the Joule findLearningPath tool + CDS pathBetween + GET /graph/path
# (#1253). Default OFF; DEV override 'true' in deploy/dev.mtaext.
KG_PATH_V2_ENABLED: 'false'
# NOTE: the on/off feature flags MCP_AUTH_ENABLED, MCP_PAT_MINT_ENABLED,
# MCP_PHASE3_ENABLED, MCP_RESOURCES_ENABLED, MCP_PROMPTS_ENABLED,
# MCP_ADMIN_TOOLS_ENABLED, METRICS_ENABLED, KG_PAGERANK_ENABLED,
# KG_PATH_V2_ENABLED, KG_RETIRE_ORPHANS_ENABLED, KG_STEP_SLICER_ENABLED,
# COMMUNITY_BLOGS_CLASSIFIER_ENABLED, HOMEPAGE_NEWS_RELEVANCE_ENABLED and
# FRESHNESS_SCAN_ENABLED used to live here. They moved to DB-driven config
# (ImsConfig flag.* rows, read via srv/lib/feature-flags/db-flags.js) in
# issue #2060 — flip them from the Admin UI / setFeatureFlag action, no
# redeploy. Defaults are seeded on boot from the registry.
# NOTE: CONTENT_API_KEY, GITHUB_DISPATCH_TOKEN, SMTP_HOST/PORT/USER/FROM
# and REBUILD_TARGET_ENV used to live here. All resolved from the BTP
# Credential Store now (via srv/lib/secret-resolver.js) or TenantSettings
Expand Down Expand Up @@ -171,7 +172,7 @@ modules:
- cp -r ../../hugo/assets ./hugo/assets
- cp -r ../../hugo/data ./hugo/data
- cp -r ../../hugo/i18n ./hugo/i18n
- bash -c "mkdir -p srv/jobs && mkdir -p srv/handlers && mkdir -p srv/lib/branch && mkdir -p srv/lib/runtime-config && mkdir -p srv/lib/prompts && mkdir -p srv/lib/kg && mkdir -p srv/mcp/prompts && cp ../../srv/lib/branch/condition.js ../../srv/lib/branch/engine.js ../../srv/lib/branch/ranker.js ../../srv/lib/branch/user-state.js ../../srv/lib/branch/loaders.js ../../srv/lib/branch/mission-detail.js ../../srv/lib/branch/slug-key.js ../../srv/lib/branch/decide.js ../../srv/lib/branch/joule-tool.js ../../srv/lib/branch/branch-telemetry.js ../../srv/lib/branch/group-by-alt.js ../../srv/lib/branch/profile-fields.js ../../srv/lib/branch/profile-override.js srv/lib/branch/ && cp ../../srv/lib/runtime-config/kg-settings.js ../../srv/lib/runtime-config/ui-events-settings.js ../../srv/lib/runtime-config/search-settings.js ../../srv/lib/runtime-config/navigator-settings.js ../../srv/lib/runtime-config/display-settings.js ../../srv/lib/runtime-config/tenant-settings.js ../../srv/lib/runtime-config/alert-settings.js srv/lib/runtime-config/ && cp ../../srv/lib/kg/on-demand-enqueue.js ../../srv/lib/kg/on-demand-cosine-rank.js srv/lib/kg/ && cp ../../srv/lib/credstore.js ../../srv/lib/secret-resolver.js ../../srv/lib/content-store.js ../../srv/lib/content-cache-coherence.js ../../srv/lib/edge-cache-headers.js ../../srv/lib/content-publish-session.js ../../srv/lib/resolve-tutorial-author.js ../../srv/lib/_tutorials-table.js ../../srv/lib/catalog-renderer.js ../../srv/lib/catalog-data.js ../../srv/lib/catalog-mission-hierarchy.js ../../srv/lib/chrome-shell.js ../../srv/lib/pipeline-log.js ../../srv/lib/legacy-id.js ../../srv/lib/embedding-pipeline.js ../../srv/lib/step-text-extractor.js ../../srv/lib/embedding-client.js ../../srv/lib/step-vectors.js ../../srv/lib/user-progress.js ../../srv/lib/co-completion.js ../../srv/lib/tutorial-centroid.js ../../srv/lib/tag-label-map.js ../../srv/lib/code-check-tool.js ../../srv/lib/code-check-prompt.js ../../srv/lib/code-check-handler.js ../../srv/lib/code-check-llm.js ../../srv/lib/code-check-step-loader.js ../../srv/lib/code-check-spec-publish.js ../../srv/lib/validate-answer-spec-publish.js ../../srv/lib/category-classifier.js ../../srv/lib/category-classifier-llm.js ../../srv/lib/category-seed-embeddings.js ../../srv/lib/build-catalog-categories.js ../../srv/lib/chat-settings-resolver.js ../../srv/lib/kg-extract.js ../../srv/lib/kg-queries.js ../../srv/lib/kg-projection.js ../../srv/lib/kg-similarity.js ../../srv/lib/kg-cycles.js ../../srv/lib/kg-graph-rebuild.js ../../srv/lib/kg-sparql-client.js ../../srv/lib/kg-merge-pair.js ../../srv/lib/kg-concept-loader.js ../../srv/lib/kg-neighborhood-cache.js ../../srv/lib/kg-neighborhood-merge.js ../../srv/lib/kg-neighborhood-full-helpers.js ../../srv/lib/kg-other-resources-loader.js ../../srv/lib/kg-stamp-meta-text.js ../../srv/lib/kg-tutorial-teaches-map.js ../../srv/lib/kg-resource-type-config.js ../../srv/lib/kg-meta-formatters.js ../../srv/lib/discovery-mission-categories.js ../../srv/lib/external-content-ttl.js ../../srv/lib/recompute-tutorial-progress-bulk-sql.js ../../srv/lib/youtube-fetcher.js ../../srv/lib/homepage-events-merger.js ../../srv/lib/homepage-rss-fetcher.js ../../srv/lib/rss-parse.js ../../srv/lib/community-blogs-fetcher.js ../../srv/lib/community-blog-source-defaults.js ../../srv/lib/community-blogs-classifier.js ../../srv/lib/safe-fetch.js ../../srv/lib/curl-transport.js ../../srv/lib/khoros-transport.js ../../srv/lib/explainer-generator.js ../../srv/lib/_token-cost.js ../../srv/lib/metrics.js ../../srv/lib/alerting.js ../../srv/lib/relevance-classifier.js ../../srv/lib/relevance-seed-embeddings.js ../../srv/lib/relevance-keyword-rules.js ../../srv/lib/canonicalize-link.js ../../srv/lib/detect-language-en.js ../../srv/lib/kg-community-coverage.js ../../srv/lib/page-key-map.js ../../srv/lib/page-fallback.js ../../srv/lib/task-record-submission-id.js ../../srv/lib/image-store.cjs ../../srv/lib/image-ingest.cjs ../../srv/lib/image-source-handler.js ../../srv/lib/img-cdn-fetch.cjs ../../srv/lib/img-cdn-retry.cjs ../../srv/lib/image-warm-utils.js ../../srv/lib/attachment-store.cjs ../../srv/lib/attachment-ingest.cjs ../../srv/lib/attachment-mime.cjs ../../srv/lib/attachment-warm-utils.js ../../srv/lib/attachment-source-handler.js ../../srv/lib/attachment-ingest-handler.js ../../srv/lib/island-manifest.json srv/lib/ && cp ../../srv/handlers/categories-after-hooks.js ../../srv/handlers/completion-path-items-altgroup.js srv/handlers/ && mkdir -p srv && cp ../../srv/content-moderation-service.js srv/ && cp ../../srv/jobs/consolidate-concepts-job.js ../../srv/jobs/extract-concepts-job.js ../../srv/jobs/job-lock.js ../../srv/jobs/secret-expiry-check.js ../../srv/jobs/homepage-link-health.js ../../srv/jobs/kg-ondemand-job.js ../../srv/jobs/community-blogs-fetch-job.js ../../srv/jobs/community-blogs-classify-job.js ../../srv/jobs/fetch-news-job.js srv/jobs/ && cp ../../srv/lib/prompts/explainer-verb.md ../../srv/lib/prompts/explainer-shelf.md ../../srv/lib/prompts/explainer-shelf-entry.md ../../srv/lib/prompts/community-blogs-classifier.md srv/lib/prompts/ && cp ../../srv/mcp/prompts/summarize_mission_for_beginner.md ../../srv/mcp/prompts/generate_lab_exercise.md ../../srv/mcp/prompts/explain_concept.md ../../srv/mcp/prompts/suggest_learning_path.md srv/mcp/prompts/"
- bash -c "mkdir -p srv/jobs && mkdir -p srv/handlers && mkdir -p srv/lib/branch && mkdir -p srv/lib/runtime-config && mkdir -p srv/lib/prompts && mkdir -p srv/lib/kg && mkdir -p srv/mcp/prompts && cp ../../srv/lib/branch/condition.js ../../srv/lib/branch/engine.js ../../srv/lib/branch/ranker.js ../../srv/lib/branch/user-state.js ../../srv/lib/branch/loaders.js ../../srv/lib/branch/mission-detail.js ../../srv/lib/branch/slug-key.js ../../srv/lib/branch/decide.js ../../srv/lib/branch/joule-tool.js ../../srv/lib/branch/branch-telemetry.js ../../srv/lib/branch/group-by-alt.js ../../srv/lib/branch/profile-fields.js ../../srv/lib/branch/profile-override.js srv/lib/branch/ && cp ../../srv/lib/runtime-config/kg-settings.js ../../srv/lib/runtime-config/ui-events-settings.js ../../srv/lib/runtime-config/search-settings.js ../../srv/lib/runtime-config/navigator-settings.js ../../srv/lib/runtime-config/display-settings.js ../../srv/lib/runtime-config/tenant-settings.js ../../srv/lib/runtime-config/alert-settings.js srv/lib/runtime-config/ && cp ../../srv/lib/kg/on-demand-enqueue.js ../../srv/lib/kg/on-demand-cosine-rank.js srv/lib/kg/ && cp ../../srv/lib/credstore.js ../../srv/lib/secret-resolver.js ../../srv/lib/content-store.js ../../srv/lib/content-delta-flags.js ../../srv/lib/content-cache-coherence.js ../../srv/lib/edge-cache-headers.js ../../srv/lib/content-publish-session.js ../../srv/lib/resolve-tutorial-author.js ../../srv/lib/_tutorials-table.js ../../srv/lib/catalog-renderer.js ../../srv/lib/catalog-data.js ../../srv/lib/catalog-mission-hierarchy.js ../../srv/lib/chrome-shell.js ../../srv/lib/pipeline-log.js ../../srv/lib/legacy-id.js ../../srv/lib/embedding-pipeline.js ../../srv/lib/step-text-extractor.js ../../srv/lib/embedding-client.js ../../srv/lib/step-vectors.js ../../srv/lib/user-progress.js ../../srv/lib/co-completion.js ../../srv/lib/tutorial-centroid.js ../../srv/lib/tag-label-map.js ../../srv/lib/code-check-tool.js ../../srv/lib/code-check-prompt.js ../../srv/lib/code-check-handler.js ../../srv/lib/code-check-llm.js ../../srv/lib/code-check-step-loader.js ../../srv/lib/code-check-spec-publish.js ../../srv/lib/validate-answer-spec-publish.js ../../srv/lib/category-classifier.js ../../srv/lib/category-classifier-llm.js ../../srv/lib/category-seed-embeddings.js ../../srv/lib/build-catalog-categories.js ../../srv/lib/chat-settings-resolver.js ../../srv/lib/kg-extract.js ../../srv/lib/kg-queries.js ../../srv/lib/kg-projection.js ../../srv/lib/kg-similarity.js ../../srv/lib/kg-cycles.js ../../srv/lib/kg-graph-rebuild.js ../../srv/lib/kg-sparql-client.js ../../srv/lib/kg-merge-pair.js ../../srv/lib/kg-concept-loader.js ../../srv/lib/kg-neighborhood-cache.js ../../srv/lib/kg-neighborhood-merge.js ../../srv/lib/kg-neighborhood-full-helpers.js ../../srv/lib/kg-other-resources-loader.js ../../srv/lib/kg-stamp-meta-text.js ../../srv/lib/kg-tutorial-teaches-map.js ../../srv/lib/kg-resource-type-config.js ../../srv/lib/kg-meta-formatters.js ../../srv/lib/discovery-mission-categories.js ../../srv/lib/external-content-ttl.js ../../srv/lib/recompute-tutorial-progress-bulk-sql.js ../../srv/lib/youtube-fetcher.js ../../srv/lib/homepage-events-merger.js ../../srv/lib/homepage-rss-fetcher.js ../../srv/lib/rss-parse.js ../../srv/lib/community-blogs-fetcher.js ../../srv/lib/community-blog-source-defaults.js ../../srv/lib/community-blogs-classifier.js ../../srv/lib/safe-fetch.js ../../srv/lib/curl-transport.js ../../srv/lib/khoros-transport.js ../../srv/lib/explainer-generator.js ../../srv/lib/_token-cost.js ../../srv/lib/metrics.js ../../srv/lib/alerting.js ../../srv/lib/relevance-classifier.js ../../srv/lib/relevance-seed-embeddings.js ../../srv/lib/relevance-keyword-rules.js ../../srv/lib/canonicalize-link.js ../../srv/lib/detect-language-en.js ../../srv/lib/kg-community-coverage.js ../../srv/lib/page-key-map.js ../../srv/lib/page-fallback.js ../../srv/lib/task-record-submission-id.js ../../srv/lib/image-store.cjs ../../srv/lib/image-ingest.cjs ../../srv/lib/image-source-handler.js ../../srv/lib/img-cdn-fetch.cjs ../../srv/lib/img-cdn-retry.cjs ../../srv/lib/image-warm-utils.js ../../srv/lib/attachment-store.cjs ../../srv/lib/attachment-ingest.cjs ../../srv/lib/attachment-mime.cjs ../../srv/lib/attachment-warm-utils.js ../../srv/lib/attachment-source-handler.js ../../srv/lib/attachment-ingest-handler.js ../../srv/lib/island-manifest.json srv/lib/ && mkdir -p srv/lib/feature-flags && cp ../../srv/lib/feature-flags/db-flags.js ../../srv/lib/feature-flags/registry.js srv/lib/feature-flags/ && cp ../../srv/handlers/categories-after-hooks.js ../../srv/handlers/completion-path-items-altgroup.js srv/handlers/ && mkdir -p srv && cp ../../srv/content-moderation-service.js srv/ && cp ../../srv/jobs/consolidate-concepts-job.js ../../srv/jobs/extract-concepts-job.js ../../srv/jobs/job-lock.js ../../srv/jobs/secret-expiry-check.js ../../srv/jobs/homepage-link-health.js ../../srv/jobs/kg-ondemand-job.js ../../srv/jobs/community-blogs-fetch-job.js ../../srv/jobs/community-blogs-classify-job.js ../../srv/jobs/fetch-news-job.js srv/jobs/ && cp ../../srv/lib/prompts/explainer-verb.md ../../srv/lib/prompts/explainer-shelf.md ../../srv/lib/prompts/explainer-shelf-entry.md ../../srv/lib/prompts/community-blogs-classifier.md srv/lib/prompts/ && cp ../../srv/mcp/prompts/summarize_mission_for_beginner.md ../../srv/mcp/prompts/generate_lab_exercise.md ../../srv/mcp/prompts/explain_concept.md ../../srv/mcp/prompts/suggest_learning_path.md srv/mcp/prompts/"
- bash -c "node -e \"const p=require('./package.json'); p.dependencies=Object.assign(p.dependencies||{},{cheerio:'^1.2.0','@sap-ai-sdk/foundation-models':'^2.10.0'}); require('fs').writeFileSync('./package.json', JSON.stringify(p,null,2));\""
properties:
EXPOSE_CAP_UI: false
Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/rebuild-content-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ on:
required: false
type: boolean
default: true
force-publish:
description: 'Seed/repair Option B ContentCurrent (QA): publish ALL slugs (dual-write everything), bypassing the changed-slug diff. Use for a one-time ContentCurrent re-seed after the delta flags were toggled. Default false. Mirrors rebuild-content.yml force-publish.'
required: false
type: boolean
default: false

env:
NODE_VERSION: '22'
Expand Down Expand Up @@ -259,10 +264,17 @@ jobs:
run: npm run build:highlight -- --dir hugo/public-qa

- name: Publish QA content to HANA
run: npx tsx scripts/publish-content.ts --channel qa --initiator "ci/${{ github.run_id }}"
run: npx tsx scripts/publish-content.ts --channel qa ${{ inputs.force-publish && '--force' || '' }} --initiator "ci/${{ github.run_id }}"
env:
CAP_QA_BASE_URL: ${{ secrets.CAP_SRV_URL_QA }}
CONTENT_API_KEY_QA: ${{ secrets.CONTENT_API_KEY_QA }}
# Scope the publish to the changed slug on a commit-triggered (or single-slug
# workflow_dispatch) rebuild — publish-content.ts reads PUBLISH_SLUG as the
# --slug fallback. Empty on a full rebuild → whole-catalog delta. Mirrors
# rebuild-content.yml. Without force-publish, this makes the commit O(changed)
# (sub-second) instead of a ~35s full-catalog write. force-publish=true ignores
# the slug and re-seeds the whole catalog (--force).
PUBLISH_SLUG: ${{ (! inputs.force-publish) && (github.event.client_payload.slug || inputs.slug) || '' }}

# #1373 — synthetic watchdog. The publish step reporting success is not
# proof rows landed: the render-concepts 404 that hid for days (#1372)
Expand Down
10 changes: 9 additions & 1 deletion app/admin-annotations.cds
Original file line number Diff line number Diff line change
Expand Up @@ -4130,7 +4130,15 @@ annotate AdminService.FeatureFlags with @UI: {
{ Value: effectiveValue, Label: 'Effective' },
{ Value: winningLayer, Label: 'Source' },
{ Value: status },
{ Value: issue }
{ Value: issue },
// #2060 — row-button toggles for kind:'db' flags (non-db flags reject 400).
{ $Type: 'UI.DataFieldForAction', Action: 'AdminService.FeatureFlags/enable', Label: 'Enable' },
{ $Type: 'UI.DataFieldForAction', Action: 'AdminService.FeatureFlags/disable', Label: 'Disable' }
],
// Object Page header actions — same enable/disable on the detail view (#2060).
Identification: [
{ $Type: 'UI.DataFieldForAction', Action: 'AdminService.FeatureFlags/enable', Label: 'Enable' },
{ $Type: 'UI.DataFieldForAction', Action: 'AdminService.FeatureFlags/disable', Label: 'Disable' }
],
Facets: [
{ $Type: 'UI.ReferenceFacet', ID: 'General', Label: 'General', Target: '@UI.FieldGroup#General' },
Expand Down
5 changes: 5 additions & 0 deletions app/admin/featureFlags/webapp/i18n/i18n.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
appTitle=Feature Flags
appSubtitle=Live state of runtime feature flags

# Bound row-action labels (#2060) — annotations use inline labels; these mirror
# them for any i18n-driven references and for translation coverage.
enableFlag=Enable
disableFlag=Disable
35 changes: 35 additions & 0 deletions approuter/lib/mcp-auth-challenge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// approuter/lib/mcp-auth-challenge.js
//
// Emits the MCP-spec-preferred discovery trigger: a 401 with a
// `WWW-Authenticate: Bearer resource_metadata="…"` pointer on the protected MCP
// namespaces, so compliant clients follow the pointer to the protected-resource
// metadata instead of blindly probing /.well-known. Runtime middleware; mirrors
// the srv-side /mcp-pat short-circuit in srv/server.js. Only fires when NO
// Authorization header is present, so a valid bearer always passes through.

const { resolveBaseUrl, resolveScope } = require('./well-known-oauth')

const MCP_AUTH_PREFIXES = ['/mcp-auth', '/mcp-admin']

function matchesProtectedMcp(pathOnly) {
return MCP_AUTH_PREFIXES.some(p => pathOnly === p || pathOnly.startsWith(p + '/'))
}

function mcpAuthChallengeHandler(req, res, next) {
const pathOnly = (req.url || '').split('?')[0]
if (!matchesProtectedMcp(pathOnly)) return next()

const authz = req.headers && req.headers.authorization
if (authz && authz.startsWith('Bearer ')) return next()

const baseUrl = resolveBaseUrl(req)
const scope = resolveScope()
if (baseUrl) {
res.setHeader('WWW-Authenticate',
`Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource", scope="${scope}"`)
}
res.writeHead(401, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'unauthorized', error_description: 'Bearer token required.' }))
}

module.exports = { mcpAuthChallengeHandler, MCP_AUTH_PREFIXES }
43 changes: 43 additions & 0 deletions approuter/lib/well-known-mcp-manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// approuter/lib/well-known-mcp-manifest.js
//
// Serves /.well-known/mcp.json — a NON-STANDARD convenience manifest listing the
// hosted MCP endpoints. NOT part of the MCP spec (server publishing is via the
// central MCP Registry's server.json); we serve it as a courtesy. Runtime
// middleware, mirroring well-known-oauth.js / security-txt.js. Base URL from the
// request; scope from the bound XSUAA binding (qualified form).

const { resolveBaseUrl, resolveScope } = require('./well-known-oauth')

const MCP_MANIFEST_PATH = '/.well-known/mcp.json'

function buildManifest(baseUrl, scope) {
return {
$comment: 'Non-standard convenience manifest; not part of the MCP specification.',
name: 'SAP Developers MCP',
provider: 'SAP Tutorials (developers.sap.com)',
servers: [
{ name: 'search', url: `${baseUrl}/mcp/search`, auth: 'none' },
{ name: 'homepage', url: `${baseUrl}/mcp/homepage`, auth: 'none' },
{ name: 'graph', url: `${baseUrl}/mcp/graph`, auth: 'none' },
{ name: 'developer', url: `${baseUrl}/mcp-auth/api`, auth: 'oauth2', scope },
],
authorization: { protected_resource: `${baseUrl}/.well-known/oauth-protected-resource` },
}
}

function sendJson(res, status, body) {
res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' })
res.end(JSON.stringify(body, null, 2))
}

function mcpManifestHandler(req, res, next) {
if (req.method !== 'GET' && req.method !== 'HEAD') return next()
const pathOnly = (req.url || '').split('?')[0]
if (pathOnly !== MCP_MANIFEST_PATH) return next()

const baseUrl = resolveBaseUrl(req)
if (!baseUrl) return sendJson(res, 503, { error: 'mcp_manifest_unavailable' })
return sendJson(res, 200, buildManifest(baseUrl, resolveScope()))
}

module.exports = { mcpManifestHandler, MCP_MANIFEST_PATH, buildManifest }
Loading
Loading