Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions openspec/changes/mcp-tool-surface/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-18
83 changes: 83 additions & 0 deletions openspec/changes/mcp-tool-surface/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Design — LaunchPad MCP Tool Surface

## Context

Product intent: every app action should be automatable by AI under per-agent, default-deny, granularly granted rights (hermiq's scope × reach model), and a user should be able to command the app from chat. LaunchPad's action surface is large (~120 routes) and today entirely invisible to MCP. The design questions were: which mechanism (provider vs schema-derived tools), how to cut ~120 routes into an honest tool catalogue, how to guarantee authorization parity, and what deliberately stays out.

The fleet reference is `decidesk/lib/Mcp/` — `DecideskToolProvider` (dispatcher + `TOOL_DESCRIPTORS` constant), `McpMeetingGate` (centralised authorise(), explicitly no fail-open), `McpArgumentValidator` (typed argument checks returning structured errors), `McpMeetingScopeResolver` (caller scoping) — registered via the string DI alias `OCA\OpenRegister\Mcp\IMcpToolProvider::decidesk`.

## Goals / Non-Goals

**Goals:**

- Full coverage of launchpad's *user* actions: 14 read + 35 write tools, ids `launchpad.{toolName}`.
- Scope (`read`/`create`/`update`/`delete`) × reach (`self`/`user`/`instance`/`external`) annotations on every tool so hermiq can grant per tool, default-deny writes.
- Authorization parity: tools call the same `PermissionService` / `ActionAuthService` guards as the HTTP controllers — one authorization truth.
- Human-approval flags on destructive (`delete`) and boundary-crossing (`external`) tools; audit-ready structured results.

**Non-Goals:**

- Building a chat UI or an agent runtime — hermiq hosts the conversation and the grant/approval/audit machinery.
- Tooling telemetry, binary uploads, editing locks, acknowledgement writes, or one-off admin plumbing (see REQ-MCP-007).
- Replacing `launchpad-ai-dashboard-assistant` — that change renders AI summaries *on* the canvas; this one puts the canvas's actions *under* AI control. No shared code.

## Decisions

### D1: Imperative provider for all writes; schema-derived tools for read discovery only

**Decision**: All writes go through `LaunchpadToolProvider` handlers calling the existing service layer. The `Dashboard` schema additionally gets an `x-openregister-mcp` block (openconnector pattern) deriving only read-only `search`/`get`.

**Alternatives considered:**

- **Schema-derived CRUD for everything** (`x-openregister-mcp` with writes enabled): rejected. Dashboard mutations are not raw object writes — `DashboardService::createDashboard()` generates slugs, `updateDashboard()` respects locks and permission levels, widget placement runs collision handling (`PlacementService`), deletes cascade. Auto-derived writes would bypass every one of those invariants.
- **Provider-only, no schema block**: workable, but the two read paths cost one small overlay and give agents uniform object discovery across the fleet. Kept.

**Rationale**: decidesk proved the provider shape; openconnector proved read-only schema blocks. LaunchPad needs both halves for its split of "rich service invariants on writes" vs "plain object reads".

### D2: One tool per user action, not per route

**Decision**: The catalogue aggregates exactly where the HTTP surface aggregates: `updateDashboard` covers rename/description/slug (as `dashboardApi#update` does), `updateWidgetPlacement` covers move/resize/restyle/reconfigure (as `widgetApi#updatePlacement` does), `setPublicationState` covers publish/unpublish/schedule (three routes, one user intent).

**Alternatives considered:**

- **One tool per route (~120 tools)**: rejected — floods the model's tool context, and route-level granularity (e.g. separate publish/unpublish tools) adds grant-matrix rows without adding a meaningfully different permission decision.
- **A handful of coarse tools ("manageDashboard")**: rejected — coarse tools break the grant model; hermiq cannot grant "reposition widgets but never delete dashboards" if both hide inside one tool.

**Rationale**: the grant matrix is the sizing instrument: two actions belong in one tool iff a rights-granter would never want to split them. Rename-vs-describe: never split. Add-widget vs delete-dashboard: always split.

### D3: Reach classification — kiosk and public links are `external`, sharing is `user`, group/template management is `instance`

**Decision**: `createPublicShareLink`, `revokePublicShareLink`, and all three kiosk playlist writes carry reach `external`; `shareDashboard`/`replaceShares`/`unshareDashboard`/`setPublicationState` carry `user`; group-dashboard CRUD, `setGroupDefault`, and template CRUD/resync carry `instance`.

**Alternatives considered:**

- **Kiosk as `self`** ("it's the caller's playlist"): rejected — a kiosk playlist mints a `#[PublicPage]` token (`kiosk#render`, `/kiosk/{token}`) that serves dashboards to anonymous viewers. What a write *exposes* determines reach, not where the record lives. Same logic as public share links.

**Rationale**: reach must encode blast radius as the rights-granter experiences it. Anything that changes who-can-see across the auth boundary is `external`; across users inside the instance is `user`; instance topology is `instance`.

### D4: Authorization is a gate class delegating to the existing guards — never re-implemented

**Decision**: `McpDashboardGate` wraps `PermissionService::can*()`, `ActionAuthService::requireAction()` (ADR-023), and the admin check; every handler consults it first. Errors deny (no `catch (\Throwable) → null`, per the unsafe-auth-resolver gate). Handlers then call `DashboardService` / `WidgetService` / `TileService` / `DashboardShareService` / `PublicShareService` / `KioskService` / `TemplateService` — never controllers (no HTTP self-calls), never mappers or `ObjectService` directly for writes.

**Rationale**: The permission logic already exists, is audited, and drifts if duplicated. decidesk's `McpMeetingGate` documents the same invariants (guards return bool, no unconditional true, no Throwable-swallowing) — codified here as spec scenarios.

### D5: Exclusions are specified, not implied

**Decision**: REQ-MCP-007 names every real endpoint family that gets no tool and why — telemetry recorders (agents must not fabricate analytics), acknowledgement writes (compliance evidence of a *human* act), binary uploads (not chat-shaped), editing locks (interactive-session mechanics), one-off admin plumbing (wizard, imports, cleanup, bulk ops, settings surfaces).

**Rationale**: an unlisted omission looks like an oversight and invites a future "completeness" PR to tool the acknowledgement endpoint — the one addition that would be actively harmful. The exclusion list makes the boundary a tested assertion (catalogue-closure scenario) instead of folklore.

## Risks / Trade-offs

- **Catalogue size (49 tools)**: large for a single provider. Mitigated by domain-grouped handler classes and the descriptor constant being a plain fixture unit tests assert against (decidesk pattern). If hermiq context budgets demand it, read tools can later be collapsed behind the schema-derived `search`/`get` without touching write semantics.
- **Grant-model drift**: scope/reach vocabulary is hermiq's; if hermiq's axes change, annotations must follow. The descriptors keep annotations as data, so this is a constant edit, not a refactor.
- **Instance-reach tools in agent hands**: template and group-dashboard tools are powerful. Defence in depth: admin gate in `McpDashboardGate` + `instance` reach requiring an explicit hermiq grant + approval gate on deletes.

## Migration Plan

Additive only: new `lib/Mcp/` classes, one DI alias line, one register.d overlay. No routes, no schema data changes. Rollback = remove the alias registration (tools disappear from the catalogue) — no data cleanup needed.

## Open Questions

- Whether hermiq wants pagination cursors on `listDashboards`/`listTemplates` results or is satisfied with the endpoint's existing limits — decide against hermiq's consumer contract during implementation.
- Whether `launchpad.forkDashboard` should accept a template *gallery id* directly or only dashboard uuids (gallery entries are dashboards, so uuid-only is likely sufficient) — confirm against `template#gallery`'s payload shape.
8 changes: 8 additions & 0 deletions openspec/changes/mcp-tool-surface/hydra.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"spec_slug": "mcp-tools",
"title": "MCP tool surface — every launchpad action commandable from chat",
"app": "launchpad",
"repo": "https://github.com/ConductionNL/launchpad",
"depends_on": [],
"issue": null
}
56 changes: 56 additions & 0 deletions openspec/changes/mcp-tool-surface/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# MCP tool surface — every launchpad action commandable from chat

## Why

The product direction is that every Conduction app exposes its actions as MCP tools, so any action can in principle be automated by an AI agent — and so a user can simply chat while their apps execute their commands: "add the calendar widget to my dashboard", "share the sprint dashboard with the sales group". hermiq consumes these tools under its two-axis **scope × reach** grant model: rights are granted per agent, granularly, with writes denied by default, human approval gates on destructive/external actions, and a full audit trail.

LaunchPad currently exposes **nothing** to that surface. Verified by grep: no `OCA\Launchpad\Mcp` namespace, no `IMcpToolProvider` implementation, no `x-openregister-mcp` block anywhere in `lib/`, `appinfo/`, or `src/` (zero hits). Meanwhile the app has one of the richest user-action surfaces in the fleet — `appinfo/routes.php` registers ~120 routes over dashboards, widgets, tiles, sharing, publication, versioning, templates, locks, and admin operations.

The fleet mechanism is proven: decidesk ships the reference implementation (`decidesk/lib/Mcp/` — `DecideskToolProvider` implementing `OCA\OpenRegister\Mcp\IMcpToolProvider`, registered under the DI alias `OCA\OpenRegister\Mcp\IMcpToolProvider::decidesk`, tool ids `decidesk.{toolName}`, with `McpMeetingGate` for authorization, `McpArgumentValidator` for input validation, and `McpMeetingScopeResolver` for caller scoping), and openconnector demonstrates schema-level `x-openregister-mcp` blocks for auto-derived read tools. This change gives launchpad the same surface, sized to its real action inventory.

**Relation to `launchpad-ai-dashboard-assistant`**: that change points AI *at* the dashboard — AI-generated summary widgets rendered on the canvas. This change points AI at launchpad's *actions* — the dashboard becomes something an agent can build, rearrange, and share on the user's behalf. They are complementary and share no code; where the assistant change's widgets need data, they read it through widget endpoints, not through these tools.

## What Changes

- New `lib/Mcp/` namespace with `LaunchpadToolProvider` implementing `OCA\OpenRegister\Mcp\IMcpToolProvider`, registered in `AppInfo\Application` under the DI alias `OCA\OpenRegister\Mcp\IMcpToolProvider::launchpad`. Tool ids are `launchpad.{toolName}`.
- A full-coverage tool catalogue derived from the real controller/service surface (see spec REQ-MCP-002/003): 14 read tools and 35 write tools covering dashboard CRUD, active/default selection, forking, widget add/reposition/remove, tile CRUD, sharing, publication state, public links, versioning, metadata, group dashboards, and admin template management.
- Read tools carry `readOnlyHint: true`; every write tool is annotated with **scope** (`create` / `update` / `delete`) and **reach** (`self` / `user` / `instance` / `external`) so hermiq's grant matrix can authorize each tool individually, default-deny.
- Authorization parity: every tool routes through the exact same guards as the HTTP surface — `PermissionService::can*()`, `ActionAuthService::requireAction()` (ADR-023 matrix), and the admin checks — via a `McpDashboardGate` mirroring decidesk's `McpMeetingGate`. Argument validation via a `McpArgumentValidator` counterpart.
- A read-only `x-openregister-mcp` block (`search`/`get`, openconnector pattern) on the `Dashboard` schema for discovery parity; auto-derived CRUD **writes stay disabled** because dashboard mutations must pass through `DashboardService` invariants (slug generation, lock checks, permission levels), never raw object writes.

## Capabilities

### New Capabilities

- `mcp-tools` — launchpad's MCP tool surface: catalogue, scope/reach annotations, authorization parity, and chat-command flows.

### Modified Capabilities

(none)

## Impact

**Affected code:**

- `lib/Mcp/LaunchpadToolProvider.php` — dispatcher + tool catalogue (new)
- `lib/Mcp/McpDashboardTools.php`, `lib/Mcp/McpWidgetTools.php`, `lib/Mcp/McpSharingTools.php`, `lib/Mcp/McpAdminTools.php` — handlers grouped by domain (new)
- `lib/Mcp/McpDashboardGate.php`, `lib/Mcp/McpArgumentValidator.php` — authorization + validation (new)
- `lib/AppInfo/Application.php` — DI alias registration (string alias, no autoload at boot, per the ADR-083-safe pattern decidesk uses)
- `lib/Settings/register.d/dashboard-mcp.json` — read-only `x-openregister-mcp` overlay (new)

**Affected APIs:**

- No new HTTP routes. Tools call the existing service layer (`DashboardService`, `WidgetService`/`WidgetPlacementService`, `TileService`, `DashboardShareService`, `PublicShareService`, `TemplateService`) — never controllers, never raw ObjectService writes.

**Dependencies:**

- OpenRegister providing `OCA\OpenRegister\Mcp\IMcpToolProvider`; hermiq as the consuming agent host. Without OpenRegister the alias resolves lazily and fails only when asked for (decidesk pattern), so launchpad still boots.

**Migration:**

- None. Additive surface; no schema or data changes beyond the read-only MCP overlay.

## Notes

- Deliberately excluded from the tool surface (REQ-MCP-007): telemetry recorders (view events, tile clicks — agents must not fabricate analytics), binary upload endpoints (resources, template preview images — chat tools take JSON, not multipart), the setup wizard, Confluence import, orphaned-data cleanup, and demo showcases (one-off admin plumbing, high blast radius, no chat use case).
- Sizing note: unlike decidesk's 5-tool exemplar, launchpad's honest action inventory is large. The catalogue is still one-tool-per-user-action, not per-route: e.g. rename/describe/re-slug are all `launchpad.updateDashboard`; move/resize/style are all `launchpad.updateWidgetPlacement` — matching how `dashboardApi#update` and `widgetApi#updatePlacement` already aggregate them.
Loading
Loading