feat(mcp): catalog preview endpoint — tools/list for a chosen role without a session (#64) - #75
Closed
nicdavidson wants to merge 1 commit into
Closed
nicdavidson wants to merge 1 commit into
nicdavidson wants to merge 1 commit into
Conversation
…ession (#64) Daemon: POST /mcp/catalog/preview (internal-key gated) takes the same _mcpConfig / _mcpAvailableServices the proxy envelopes, plus clientName and lazyMode overrides, and runs the real registration path (createServer) in a throwaway in-memory server. Returns { tools[{name,title,description,category, write,service?}], count, bytes, lazy, facade }. No DreamFactory calls, no session, nothing executed. The envelope config parser is shared with the proxied request (parseMcpConfig) so the preview cannot drift from a session. PHP: admin-only GET /_internal/ai/mcp-catalog?service=<name|id>&role_id=<id> |app_id=<id>[&client=][&lazy_mode=]. Seeds the session as that role (RoleSession: no user, app id, Role::getCachedInfo — the key-auth composition), resolves the catalog through the shared AvailableServices::resolve(), reads the role's verb mask per backend, asks the daemon for the preview, and restores the admin's session. No audit row. Tests: daemon unit test for the preview (prefixed vs merged, lazy on/auto/off + override, passthrough client, disabled_tools, custom tools, empty exposure); PHP wiring test (route, admin gate, shared resolver, no proxy / rpc / audit) and a functional RoleSession test on the real df-core Session.
Contributor
Author
|
Integrated into the team branch |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The admin UI needs to show what a role or API key will get from
tools/liston an MCP service before anyone connects. The verb lists, prefix rules, aggregator thresholds, facade names and the lazy decision live in the daemon, so the daemon computes the list and PHP supplies what the proxy would have sent for that role.Daemon —
POST /mcp/catalog/preview(internal-key gated like/mcp/cache/clear). Body:{ serviceName, _mcpConfig, _mcpAvailableServices, clientName?, lazyMode? }, the same envelope fields the proxy sends. Runs the real registration path (createServer) in a throwaway in-memory server, lists tools with the given client name, and returns{ tools: [{name,title,description,category,write,service?}], count, bytes, lazy: 'lazy'|'direct'|'passthrough', facade: [...] }.toolsis the full callable catalog (facade excluded),bytesits serializedtools/listsize (theautothreshold input),facadewhat the client actually sees instead when the decision islazy(facade + hot tools). No DreamFactory calls, no session, nothing executed. The envelope config parsing was lifted out of the request handler intoparseMcpConfig()and shared, so the preview cannot drift from a session.PHP — admin-only
GET /_internal/ai/mcp-catalog?service=<name|id>&role_id=<id>|app_id=<id>[&client=<name>][&lazy_mode=auto|on|off](registered underdf.auth_checknext tomcp-usage). It seeds the DF session as that role throughSupport\RoleSession(no user soisSysAdmin()is false, app id,Role::getCachedInfo()— the same composition the API-key path establishes viasetSessionData()), resolves the catalog through the sharedAvailableServices::resolve(), readsSession::getServicePermissions()per backend, calls the daemon preview viaMcpDaemonClient::catalogPreview(), and restores the admin's session infinally. The response addsservice,role,client,lazy_mode,tool_styleandbackends: [{name,type,category,verbs,component_scoped,components}]so the UI can mark tools that are advertised but denied at call time.verbsis the union of verb masks over every role row for that backend at any component (follow-up commit: the service-wide lookup alone reported[]for roles granted only at table level);component_scopedis true when the role has no service-wide row for the backend, andcomponentslists the component patterns the grant is limited to, e.g.["_table/orders/*", "_table/customers/*"].app_idpreviews the app's default role (what an API-key-only client runs as). Only data-planemcpservices are accepted;system_mcpreturns 400. Nomcp_request_logrow is written.README (both) document the endpoint and response.
Behaviour changes
None for MCP clients: the proxied
/mcp/{service}path is unchanged (the config parse was moved into a function, byte-for-byte the same logic), existing daemon tests pass unchanged.tools/listas the calling admin, which needed a daemon session and wrote an audit row.GET /_internal/ai/mcp-catalogand a new internal daemon routePOST /mcp/catalog/preview. Neither opens a session, runs a tool or logs tomcp_request_log. The preview mutates the admin's session only for the duration of the call and restores it.MCP_INTERNAL_KEYis set, the new daemon route requires it like the other internal routes.MCP_INTERNAL_KEY(403 without the key, JSON preview with it,X-Mcp-Ledgerstripped); PHP functional test of the role session on the real df-coreSession; full PHP suite.Testing
daemon/src/services/catalog-preview.test.ts(6 tests): prefixed classification (category / write / service, longest-prefix binding, aggregators), merged style, lazyon,autosmall vs large,lazyModeoverride, passthrough client,disabled_tools+ custom tools (api GET/POST, function, disabled), empty / missing exposure stays empty.tests/Unit/CatalogPreviewWiringTest.php: route underdf.auth_check, admin gate first, exactly oneAvailableServices::resolve()insideRoleSession::run(), verb masks read as the role, noproxyRequest/rpcStateless/RequestLogger/_mcpPayload, daemon route key-gated and built oncreateServer+parseMcpConfig, noDreamFactoryServiceand nocallToolin the preview.tests/Unit/RoleSessionTest.php(functional, real df-coreSession): a component-only role (orders_analyst: GET on_table/orders/*, GET+POST on_table/customers/*) reportsverbs: [GET, POST],component_scoped: true,components: [...]while df-core's service-wide lookup returns 0; script-only rows ignored; inactive role reports nothing; inside the callbackisSysAdmin()is false, the catalog is role ∩ exposure, per-backend verb masks are the role's; afterwards the admin session is byte-identical, also when the callback throws.OK (188 tests, 671 assertions)(was 180).npm test13 pass / 4 fail — the 4 failures are pre-existing ondevelop(thelist_toolsfacade name missing from two expected arrays) and are fixed by test(daemon): expect list_tools in the lazy facade catalog #62; with test(daemon): expect list_tools in the lazy facade catalog #62's two-line diff applied locally the suite is 17/17.tsc --noEmitclean.Closes #64