Why
The MCP service page currently synthesises its "Built-in Tools" list client-side from EVERY active database/file service and ignores exposed_services, so it shows tools the server does not expose (demo_mcp exposes 3 databases, page shows 10 services all on). The health banner says "No role grants access, unreachable" although any authenticated user can connect. Admins cannot see what a given role will actually get, who can connect, or what lazy loading / merged style / API-key auth change. Customer research (12 calls): "limit what the LLM can see/do" is the #1 ask on every call; setup friction and tool-count confusion are top-8.
An interactive mockup of the target page exists (design reference, not pixel spec): it will be provided in the worktree as .agent-mockup.html (open it in a browser). Keep the existing admin design system (Material, purple primary, Inter).
Backend contracts (df-mcp-server, branches in flight; code against these shapes and degrade gracefully if an endpoint is missing)
- Service config fields on type
mcp (GET /api/v2/system/service/{id} -> config): exposed_services: string[], disabled_tools: string[] (keys are <sanitized_service>_<verb> in BOTH styles), lazy_mode: 'auto'|'on'|'off', tool_style: 'merged'|'prefixed' (merged is the new default for new services), allow_api_key_auth: bool, require_role_access: bool (new; off on upgraded servers, on for new ones), allow_writes: bool (new, default true), custom_tools, oauth_client_id, redirect_uris, registered_redirect_uris. Schema: GET /api/v2/system/service_type/mcp (config_schema, incl. exposed_services.values = attachable DB/file services).
GET /_internal/ai/mcp-access?service=<name|id>&period=30d (admin) -> { service_id, service, require_role_access, roles: [{ role_id, name, granted, requests, denied, last_seen }] }.
GET /_internal/ai/mcp-catalog?service=<name|id>&role_id=<id>|app_id=<id>&client=<name> (admin, PR in flight) -> { tools: [{ name, title, description, category ('database'|'file'|'global'|'aggregate'|'custom'|'facade'), write: bool, service?: string }], count, bytes, lazy: 'lazy'|'direct'|'passthrough', facade: string[], role_verbs?: { [service]: verb_mask } }. Until it exists, fall back to POST /api/v2/{svc}/rpc tools/list (admin view only) and say so in the UI.
GET /_internal/ai/mcp-usage?period=7d&service_id= (exists) -> by_tool, by_role, by_app, by_client, series.
GET /_internal/ai/mcp-health (admin, PR in flight) -> { status, checks: [{ id, status, message }], daemons: [{ type, reachable, ... }] }.
- Roles:
GET/PATCH /api/v2/system/role/{id}?related=role_service_access_by_role_id (grant = row { service_id, component: '*', verb_mask: 1, requestor_mask: 3 }; unlink = send the row with role_id: null). Apps/keys: /api/v2/system/app.
What to build (edit page for type mcp; system_mcp gets the same minus exposure grid)
- Header: name, endpoint URL, health chip (from mcp-health; link to the System page), Preview-as selector (Any admin | each role from mcp-access + each app key), summary tiles: tools advertised, est. tokens per turn (bytes/4), catalog mode (Direct/Lazy + why), who can connect (OAuth only / OAuth + key).
- Modes strip: Lazy (Auto/On/Off), Tool style (Merged/Prefixed legacy), Writes (Allowed/Blocked = allow_writes), Authentication (OAuth only / OAuth + API key), Require role access (On/Off) — each with a one-line consequence that updates live from the catalog preview.
- What the client carries each turn: two bars (full catalog bytes vs 5-tool facade) against the 32 KB auto threshold, and the 4 nested steps search_tools -> describe_tool -> call_tool -> fetch_more, greyed when the server is direct.
- Who can connect: rows from mcp-access (name, read/write, reach across exposed APIs, key, requests/denied, Preview as). Actions: Add existing role (picker; grants ONLY the server unless the admin ticks specific APIs), Edit access (per exposed API None/Read/Read+write with a plain diff before save; table-level limits link to the role page), Create role (name, per-API read/read-write, optional API key shown once). Every save that touches an API grant states "this changes what the role can do everywhere, not only through MCP". With require_role_access OFF show the warning "Any logged-in user can connect" and the roles seen in the last 30 days without a grant, each with Grant.
- Exposure grid: rows = attachable backends grouped by type (Database 16 verbs grouped Read/Spec/Procedures/Write, File 6 verbs), row switch = exposed_services, cell = disabled_tools; write verbs amber with a W badge; cell states: on, off (disabled), writes-off (server switch), denied (advertised but the previewed role has no verb), gone (row hidden from this role). Legend. Replace the current synthesised Built-in Tools panels; keep custom tools editor as is.
- Catalog panel: the previewed identity's tool chips (W badge on writes, facade chips distinct), counts.
- Connect card: config snippets for Claude, Cursor, VS Code, ChatGPT, curl, switching with the auth mode and selected identity (key header when a key is previewed).
- Last 7 days: per exposed backend calls + last used from mcp-usage; zero calls highlighted.
- Create flow for a new
mcp service: short stepper: name -> attach APIs -> who can connect -> client settings -> review (this page in preview mode) -> connect snippet with a "waiting for the first call" state.
Fix on the way: the create link in df-ai-mcp-servers points at the database type list; system_mcp must not show Tool style.
Constraints
- Angular standalone components under
src/app/adf-ai/mcp/ (or shared/components/df-mcp-*), composed into df-service-details for the MCP group behind the existing isMcp branch; keep the monolith edits small. i18n strings in src/assets/i18n/en.json. Unit specs for the pure logic (catalog shaping, diff text, snippet generation) with the repo's test runner; add one Playwright e2e under e2e/ that logs in, opens demo_mcp on the dev instance (http://localhost:8081, admin@dreamfactory.com / passwordpassword) and asserts the grid reflects exposed_services.
- The repo commits
dist/; rebuild it (npm run build) in a final "chore: rebuild dist" commit so the dev instance can serve it.
- Lint + tests green; CI runs on PRs.
Behaviour changes
- The MCP service page shows only what the server actually exposes; tool toggles map 1:1 to
disabled_tools keys; new controls for require_role_access and allow_writes; role grants can be created/edited from the page (writes real role rows, with a diff shown first).
Why
The MCP service page currently synthesises its "Built-in Tools" list client-side from EVERY active database/file service and ignores
exposed_services, so it shows tools the server does not expose (demo_mcp exposes 3 databases, page shows 10 services all on). The health banner says "No role grants access, unreachable" although any authenticated user can connect. Admins cannot see what a given role will actually get, who can connect, or what lazy loading / merged style / API-key auth change. Customer research (12 calls): "limit what the LLM can see/do" is the #1 ask on every call; setup friction and tool-count confusion are top-8.An interactive mockup of the target page exists (design reference, not pixel spec): it will be provided in the worktree as
.agent-mockup.html(open it in a browser). Keep the existing admin design system (Material, purple primary, Inter).Backend contracts (df-mcp-server, branches in flight; code against these shapes and degrade gracefully if an endpoint is missing)
mcp(GET /api/v2/system/service/{id}->config):exposed_services: string[],disabled_tools: string[](keys are<sanitized_service>_<verb>in BOTH styles),lazy_mode: 'auto'|'on'|'off',tool_style: 'merged'|'prefixed'(merged is the new default for new services),allow_api_key_auth: bool,require_role_access: bool(new; off on upgraded servers, on for new ones),allow_writes: bool(new, default true),custom_tools,oauth_client_id,redirect_uris,registered_redirect_uris. Schema:GET /api/v2/system/service_type/mcp(config_schema, incl.exposed_services.values= attachable DB/file services).GET /_internal/ai/mcp-access?service=<name|id>&period=30d(admin) ->{ service_id, service, require_role_access, roles: [{ role_id, name, granted, requests, denied, last_seen }] }.GET /_internal/ai/mcp-catalog?service=<name|id>&role_id=<id>|app_id=<id>&client=<name>(admin, PR in flight) ->{ tools: [{ name, title, description, category ('database'|'file'|'global'|'aggregate'|'custom'|'facade'), write: bool, service?: string }], count, bytes, lazy: 'lazy'|'direct'|'passthrough', facade: string[], role_verbs?: { [service]: verb_mask } }. Until it exists, fall back toPOST /api/v2/{svc}/rpctools/list(admin view only) and say so in the UI.GET /_internal/ai/mcp-usage?period=7d&service_id=(exists) ->by_tool,by_role,by_app,by_client,series.GET /_internal/ai/mcp-health(admin, PR in flight) ->{ status, checks: [{ id, status, message }], daemons: [{ type, reachable, ... }] }.GET/PATCH /api/v2/system/role/{id}?related=role_service_access_by_role_id(grant = row{ service_id, component: '*', verb_mask: 1, requestor_mask: 3 }; unlink = send the row withrole_id: null). Apps/keys:/api/v2/system/app.What to build (edit page for type
mcp;system_mcpgets the same minus exposure grid)mcpservice: short stepper: name -> attach APIs -> who can connect -> client settings -> review (this page in preview mode) -> connect snippet with a "waiting for the first call" state.Fix on the way: the create link in
df-ai-mcp-serverspoints at the database type list;system_mcpmust not show Tool style.Constraints
src/app/adf-ai/mcp/(orshared/components/df-mcp-*), composed intodf-service-detailsfor the MCP group behind the existingisMcpbranch; keep the monolith edits small. i18n strings insrc/assets/i18n/en.json. Unit specs for the pure logic (catalog shaping, diff text, snippet generation) with the repo's test runner; add one Playwright e2e undere2e/that logs in, opens demo_mcp on the dev instance (http://localhost:8081, admin@dreamfactory.com / passwordpassword) and asserts the grid reflectsexposed_services.dist/; rebuild it (npm run build) in a final "chore: rebuild dist" commit so the dev instance can serve it.Behaviour changes
disabled_toolskeys; new controls for require_role_access and allow_writes; role grants can be created/edited from the page (writes real role rows, with a diff shown first).